@ouro.bot/cli 0.1.0-alpha.793 → 0.1.0-alpha.794

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.
@@ -1127,6 +1127,11 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1127
1127
  let providerIterations = 0;
1128
1128
  const requiredToolCallNames = [...new Set(options?.requiredToolCalls?.names ?? [])];
1129
1129
  const dispatchedRequiredToolCalls = new Set();
1130
+ const requiredCorrectionMarker = Symbol("requiredCorrection");
1131
+ const messagesWithoutRequiredCorrections = () => messages.filter((message) => message[requiredCorrectionMarker] !== true);
1132
+ const removeRequiredCorrections = () => {
1133
+ messages.splice(0, messages.length, ...messagesWithoutRequiredCorrections());
1134
+ };
1130
1135
  const pendingRequiredToolCalls = () => {
1131
1136
  const missing = requiredToolCallNames.filter((name) => !dispatchedRequiredToolCalls.has(name));
1132
1137
  return missing.length > 0
@@ -1136,7 +1141,12 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1136
1141
  const queueRequiredCorrection = (message, limitContext) => {
1137
1142
  if (providerIterations >= exports.MAX_PROVIDER_ITERATIONS)
1138
1143
  throw new Error(`provider iteration limit exhausted at response ${exports.MAX_PROVIDER_ITERATIONS} ${limitContext}`);
1139
- messages.push({ role: "user", content: message });
1144
+ const correction = {
1145
+ role: "user",
1146
+ content: message,
1147
+ [requiredCorrectionMarker]: true,
1148
+ };
1149
+ messages.push(correction);
1140
1150
  providerRuntime.resetTurnState(messages);
1141
1151
  };
1142
1152
  const toolLoopState = (0, tool_loop_1.createToolLoopState)();
@@ -1211,307 +1221,398 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1211
1221
  // Rebase provider-owned turn state from canonical messages at user-turn start.
1212
1222
  // This prevents stale provider caches from replaying prior-turn context.
1213
1223
  providerRuntime.resetTurnState(messages);
1214
- while (!done) {
1215
- // Channel-based tool filtering:
1216
- // - Private runtime: exclude send_message (delivery via surface), observe (no one to observe)
1217
- // - All outward channels (1:1, group, reaction): observe available
1218
- //
1219
- // ponder, settle/rest, surface, and observe are always assembled based on channel context.
1220
- // ponder is available in ALL channels (outer: think privately, inner: keep turning).
1221
- // Private runtime gets restTool instead of settleTool (rest = end turn, gated by attention queue).
1222
- // toolChoiceRequired only controls whether tool_choice: "required" is set in the API call.
1223
- const isPrivateRuntimeChannel = channel === "inner";
1224
- const privateRuntimeHabitCanSendMessage = isPrivateRuntimeChannel
1225
- && habitSession?.toolPolicy.outwardMessagingAllowed === true
1226
- && habitSession.toolPolicy.grantedTools.includes("send_message");
1227
- const privateRuntimeHabitCanSurface = isPrivateRuntimeChannel
1228
- && (!habitSession || (habitSession.toolPolicy.outwardMessagingAllowed === true
1229
- && habitSession.toolPolicy.grantedTools.includes("surface")));
1230
- const filteredBaseTools = isPrivateRuntimeChannel
1231
- ? baseTools.filter((t) => privateRuntimeHabitCanSendMessage || t.function.name !== "send_message")
1232
- : baseTools;
1233
- const unscopedOrdinaryActiveTools = [
1234
- ...filteredBaseTools,
1235
- ...(augmentedToolContext?.noSend === true ? [] : [tools_1.ponderTool]),
1236
- ...(isPrivateRuntimeChannel && privateRuntimeHabitCanSurface ? [tools_2.surfaceToolDef] : []),
1237
- ...(isPrivateRuntimeChannel ? [tools_1.restTool] : []),
1238
- ...(!isPrivateRuntimeChannel ? [tools_1.observeTool] : []),
1239
- ...(!isPrivateRuntimeChannel ? [tools_1.settleTool] : []),
1240
- ...(isChatStyleChannel(channel ?? "") ? [tools_1.speakTool] : []),
1241
- ];
1242
- const ordinaryActiveTools = relationshipToolNames
1243
- ? unscopedOrdinaryActiveTools.filter((tool) => relationshipToolNames.includes(tool.function.name))
1244
- : unscopedOrdinaryActiveTools;
1245
- const candidateActiveTools = options?.toolProfile === "sanctuary-health-private"
1246
- ? (() => {
1247
- const sendTools = baseTools.filter((tool) => tool.function.name === "send_message");
1248
- if (channel !== "inner" || sendTools.length !== 1 || baseTools.length !== 1) {
1249
- throw new Error("sanctuary-health-private requires inner channel with exactly one canonical send_message definition");
1250
- }
1251
- return [sendTools[0], tools_1.restTool];
1252
- })()
1253
- : ordinaryActiveTools;
1254
- const candidateToolNames = new Set(candidateActiveTools.map((tool) => tool.function.name));
1255
- const unadvertisedRequiredTool = requiredToolCallNames.find((name) => !candidateToolNames.has(name));
1256
- if (unadvertisedRequiredTool) {
1257
- (0, runtime_1.emitNervesEvent)({ level: "error", component: "engine", event: "engine.required_tool_unadvertised", message: "required tool is not advertised for the active channel", meta: { toolName: unadvertisedRequiredTool, channel: String(channel) } });
1258
- throw new Error(`required tool is not advertised for this channel: ${unadvertisedRequiredTool}`);
1259
- }
1260
- const forcedHistoricalToolNames = new Set(unresolvedHistoricalEffects
1261
- .map((effect) => effect.name)
1262
- .filter((name) => candidateToolNames.has(name)));
1263
- const forcingHistoricalEffect = forcedHistoricalToolNames.size > 0;
1264
- const activeTools = forcingHistoricalEffect
1265
- ? candidateActiveTools.filter((tool) => forcedHistoricalToolNames.has(tool.function.name))
1266
- : candidateActiveTools;
1267
- const activeToolNames = new Set(activeTools.map((tool) => tool.function.name));
1268
- const steeringFollowUps = options?.drainSteeringFollowUps?.() ?? [];
1269
- if (steeringFollowUps.length > 0) {
1270
- const hasSupersedingFollowUp = steeringFollowUps.some((followUp) => followUp.effect === "clear_and_supersede");
1271
- if (hasSupersedingFollowUp) {
1272
- mustResolveBeforeHandoffActive = false;
1273
- options?.setMustResolveBeforeHandoff?.(false);
1274
- outcome = "superseded";
1275
- break;
1224
+ try {
1225
+ while (!done) {
1226
+ // Channel-based tool filtering:
1227
+ // - Private runtime: exclude send_message (delivery via surface), observe (no one to observe)
1228
+ // - All outward channels (1:1, group, reaction): observe available
1229
+ //
1230
+ // ponder, settle/rest, surface, and observe are always assembled based on channel context.
1231
+ // ponder is available in ALL channels (outer: think privately, inner: keep turning).
1232
+ // Private runtime gets restTool instead of settleTool (rest = end turn, gated by attention queue).
1233
+ // toolChoiceRequired only controls whether tool_choice: "required" is set in the API call.
1234
+ const isPrivateRuntimeChannel = channel === "inner";
1235
+ const privateRuntimeHabitCanSendMessage = isPrivateRuntimeChannel
1236
+ && habitSession?.toolPolicy.outwardMessagingAllowed === true
1237
+ && habitSession.toolPolicy.grantedTools.includes("send_message");
1238
+ const privateRuntimeHabitCanSurface = isPrivateRuntimeChannel
1239
+ && (!habitSession || (habitSession.toolPolicy.outwardMessagingAllowed === true
1240
+ && habitSession.toolPolicy.grantedTools.includes("surface")));
1241
+ const filteredBaseTools = isPrivateRuntimeChannel
1242
+ ? baseTools.filter((t) => privateRuntimeHabitCanSendMessage || t.function.name !== "send_message")
1243
+ : baseTools;
1244
+ const unscopedOrdinaryActiveTools = [
1245
+ ...filteredBaseTools,
1246
+ ...(augmentedToolContext?.noSend === true ? [] : [tools_1.ponderTool]),
1247
+ ...(isPrivateRuntimeChannel && privateRuntimeHabitCanSurface ? [tools_2.surfaceToolDef] : []),
1248
+ ...(isPrivateRuntimeChannel ? [tools_1.restTool] : []),
1249
+ ...(!isPrivateRuntimeChannel ? [tools_1.observeTool] : []),
1250
+ ...(!isPrivateRuntimeChannel ? [tools_1.settleTool] : []),
1251
+ ...(isChatStyleChannel(channel ?? "") ? [tools_1.speakTool] : []),
1252
+ ];
1253
+ const ordinaryActiveTools = relationshipToolNames
1254
+ ? unscopedOrdinaryActiveTools.filter((tool) => relationshipToolNames.includes(tool.function.name))
1255
+ : unscopedOrdinaryActiveTools;
1256
+ const candidateActiveTools = options?.toolProfile === "sanctuary-health-private"
1257
+ ? (() => {
1258
+ const sendTools = baseTools.filter((tool) => tool.function.name === "send_message");
1259
+ if (channel !== "inner" || sendTools.length !== 1 || baseTools.length !== 1) {
1260
+ throw new Error("sanctuary-health-private requires inner channel with exactly one canonical send_message definition");
1261
+ }
1262
+ return [sendTools[0], tools_1.restTool];
1263
+ })()
1264
+ : ordinaryActiveTools;
1265
+ const candidateToolNames = new Set(candidateActiveTools.map((tool) => tool.function.name));
1266
+ const unadvertisedRequiredTool = requiredToolCallNames.find((name) => !candidateToolNames.has(name));
1267
+ if (unadvertisedRequiredTool) {
1268
+ (0, runtime_1.emitNervesEvent)({ level: "error", component: "engine", event: "engine.required_tool_unadvertised", message: "required tool is not advertised for the active channel", meta: { toolName: unadvertisedRequiredTool, channel: String(channel) } });
1269
+ throw new Error(`required tool is not advertised for this channel: ${unadvertisedRequiredTool}`);
1276
1270
  }
1277
- if (steeringFollowUps.some((followUp) => followUp.effect === "set_no_handoff")) {
1278
- mustResolveBeforeHandoffActive = true;
1279
- options?.setMustResolveBeforeHandoff?.(true);
1271
+ const forcedHistoricalToolNames = new Set(unresolvedHistoricalEffects
1272
+ .map((effect) => effect.name)
1273
+ .filter((name) => candidateToolNames.has(name)));
1274
+ const forcingHistoricalEffect = forcedHistoricalToolNames.size > 0;
1275
+ const activeTools = forcingHistoricalEffect
1276
+ ? candidateActiveTools.filter((tool) => forcedHistoricalToolNames.has(tool.function.name))
1277
+ : candidateActiveTools;
1278
+ const activeToolNames = new Set(activeTools.map((tool) => tool.function.name));
1279
+ const steeringFollowUps = options?.drainSteeringFollowUps?.() ?? [];
1280
+ if (steeringFollowUps.length > 0) {
1281
+ const hasSupersedingFollowUp = steeringFollowUps.some((followUp) => followUp.effect === "clear_and_supersede");
1282
+ if (hasSupersedingFollowUp) {
1283
+ mustResolveBeforeHandoffActive = false;
1284
+ options?.setMustResolveBeforeHandoff?.(false);
1285
+ outcome = "superseded";
1286
+ break;
1287
+ }
1288
+ if (steeringFollowUps.some((followUp) => followUp.effect === "set_no_handoff")) {
1289
+ mustResolveBeforeHandoffActive = true;
1290
+ options?.setMustResolveBeforeHandoff?.(true);
1291
+ }
1292
+ sawSteeringFollowUp = true;
1293
+ for (const followUp of steeringFollowUps) {
1294
+ messages.push({ role: "user", content: followUp.text });
1295
+ }
1296
+ providerRuntime.resetTurnState(messages);
1280
1297
  }
1281
- sawSteeringFollowUp = true;
1282
- for (const followUp of steeringFollowUps) {
1283
- messages.push({ role: "user", content: followUp.text });
1298
+ // Yield so pending I/O (stdin Ctrl-C) can be processed between iterations
1299
+ await new Promise((r) => setImmediate(r));
1300
+ if (signal?.aborted) {
1301
+ outcome = "aborted";
1302
+ break;
1284
1303
  }
1285
- providerRuntime.resetTurnState(messages);
1286
- }
1287
- // Yield so pending I/O (stdin Ctrl-C) can be processed between iterations
1288
- await new Promise((r) => setImmediate(r));
1289
- if (signal?.aborted) {
1290
- outcome = "aborted";
1291
- break;
1292
- }
1293
- try {
1294
- const turnCallbackBufferRef = { current: null };
1295
- const callProviderTurn = async () => {
1296
- callbacks.onModelStart();
1297
- turnCallbackBufferRef.current = habitSession
1298
- ? createHabitCallbackBuffer(callbacks)
1299
- : callbacks.settleOutputMode === "final_only"
1300
- ? createFinalOnlyTextBuffer(callbacks)
1301
- : null;
1302
- try {
1303
- const promptBudget = (0, prompt_budget_1.applyPromptBudget)({
1304
- messages,
1305
- requiredPromptEvidence: options?.requiredPromptEvidence,
1306
- provider: providerRuntime.id,
1307
- model: providerRuntime.model,
1308
- contextWindowTokens: (0, config_1.getContextConfig)().maxTokens,
1309
- });
1310
- if (promptBudget.status !== "within_budget") {
1311
- messages.splice(0, messages.length, ...promptBudget.messages);
1312
- providerRuntime.resetTurnState(messages);
1304
+ try {
1305
+ const turnCallbackBufferRef = { current: null };
1306
+ const callProviderTurn = async () => {
1307
+ callbacks.onModelStart();
1308
+ turnCallbackBufferRef.current = habitSession
1309
+ ? createHabitCallbackBuffer(callbacks)
1310
+ : callbacks.settleOutputMode === "final_only"
1311
+ ? createFinalOnlyTextBuffer(callbacks)
1312
+ : null;
1313
+ try {
1314
+ const promptBudget = (0, prompt_budget_1.applyPromptBudget)({
1315
+ messages,
1316
+ requiredPromptEvidence: options?.requiredPromptEvidence,
1317
+ provider: providerRuntime.id,
1318
+ model: providerRuntime.model,
1319
+ contextWindowTokens: (0, config_1.getContextConfig)().maxTokens,
1320
+ });
1321
+ if (promptBudget.status !== "within_budget") {
1322
+ messages.splice(0, messages.length, ...promptBudget.messages);
1323
+ providerRuntime.resetTurnState(messages);
1324
+ }
1325
+ return await providerRuntime.streamTurn({
1326
+ messages,
1327
+ activeTools,
1328
+ callbacks: turnCallbackBufferRef.current?.callbacks ?? callbacks,
1329
+ signal,
1330
+ traceId,
1331
+ toolChoiceRequired: forcingHistoricalEffect || toolChoiceRequired,
1332
+ reasoningEffort: currentReasoningEffort,
1333
+ eagerSettleStreaming: true,
1334
+ systemPrompt: structuredSystemPrompt,
1335
+ });
1336
+ }
1337
+ catch (error) {
1338
+ turnCallbackBufferRef.current?.discard();
1339
+ turnCallbackBufferRef.current = null;
1340
+ if (signal?.aborted)
1341
+ throw new provider_attempt_1.ProviderAttemptAbortError();
1342
+ throw error;
1313
1343
  }
1314
- return await providerRuntime.streamTurn({
1315
- messages,
1316
- activeTools,
1317
- callbacks: turnCallbackBufferRef.current?.callbacks ?? callbacks,
1318
- signal,
1319
- traceId,
1320
- toolChoiceRequired: forcingHistoricalEffect || toolChoiceRequired,
1321
- reasoningEffort: currentReasoningEffort,
1322
- eagerSettleStreaming: true,
1323
- systemPrompt: structuredSystemPrompt,
1324
- });
1344
+ };
1345
+ const callProviderTurnWithOverflowRecovery = async () => {
1346
+ try {
1347
+ return await callProviderTurn();
1348
+ }
1349
+ catch (error) {
1350
+ if (error instanceof provider_attempt_1.ProviderAttemptAbortError)
1351
+ throw error;
1352
+ if (isContextOverflow(error) && !overflowRetried) {
1353
+ overflowRetried = true;
1354
+ stripLastToolCalls(messages);
1355
+ stripLastToolCalls(generatedMessages);
1356
+ const { maxTokens, contextMargin } = (0, config_1.getContextConfig)();
1357
+ const trimmed = (0, context_1.trimMessages)(messages, maxTokens, contextMargin, maxTokens * 2);
1358
+ const requiredEvidence = options?.requiredPromptEvidence;
1359
+ const requiredMessages = new Set([
1360
+ ...(requiredEvidence?.verifiedPredecessorMessage ? [requiredEvidence.verifiedPredecessorMessage] : []),
1361
+ ...(requiredEvidence?.currentUserMessage ? [requiredEvidence.currentUserMessage] : []),
1362
+ ]);
1363
+ const trimmedMessages = new Set(trimmed);
1364
+ const overflowRetryMessages = requiredMessages.size === 0
1365
+ ? trimmed
1366
+ : messages.filter((message) => trimmedMessages.has(message) || requiredMessages.has(message));
1367
+ messages.splice(0, messages.length, ...overflowRetryMessages);
1368
+ providerRuntime.resetTurnState(messages);
1369
+ callbacks.onError(new Error("context trimmed, retrying..."), "transient");
1370
+ return callProviderTurn();
1371
+ }
1372
+ throw error;
1373
+ }
1374
+ };
1375
+ const attempt = await (0, provider_attempt_1.runProviderAttempt)({
1376
+ operation: "turn",
1377
+ provider: providerRuntime.id,
1378
+ model: providerRuntime.model,
1379
+ run: callProviderTurnWithOverflowRecovery,
1380
+ classifyError: (error) => providerRuntime.classifyError(error),
1381
+ onRetry: async (record, maxAttempts) => {
1382
+ const delayMs = record.delayMs;
1383
+ const seconds = delayMs / 1000;
1384
+ const cause = RETRY_LABELS[record.classification];
1385
+ try {
1386
+ if (record.provider === "openai-codex" && record.classification === "auth-failure") {
1387
+ await (0, openai_codex_token_1.refreshOpenAICodexProviderCredentials)((0, identity_2.getAgentName)(), {
1388
+ force: true,
1389
+ reason: "turn-auth-failure",
1390
+ });
1391
+ }
1392
+ await (0, provider_credentials_1.refreshProviderCredentialPool)((0, identity_2.getAgentName)(), {
1393
+ preserveCachedOnFailure: true,
1394
+ providers: [record.provider],
1395
+ });
1396
+ _providerRuntimeFactories[facing] = null;
1397
+ providerRuntime = await getProviderRuntime(facing);
1398
+ providerRuntime.resetTurnState(messages);
1399
+ }
1400
+ catch (refreshError) {
1401
+ (0, runtime_1.emitNervesEvent)({
1402
+ level: "warn",
1403
+ component: "engine",
1404
+ event: "engine.provider_retry_refresh_failed",
1405
+ message: "provider credential refresh failed during retry",
1406
+ meta: { provider: record.provider, model: record.model, reason: refreshError instanceof Error ? refreshError.message : String(refreshError) },
1407
+ });
1408
+ }
1409
+ callbacks.onError(new Error(`${cause}, retrying in ${seconds}s (${record.attempt}/${maxAttempts})...`), "transient");
1410
+ },
1411
+ sleep: async (delayMs) => {
1412
+ await waitForProviderRetry(delayMs, signal);
1413
+ providerRuntime.resetTurnState(messages);
1414
+ },
1415
+ });
1416
+ if (!attempt.ok) {
1417
+ finishTerminalProviderError(attempt.error, attempt.classification);
1418
+ continue;
1325
1419
  }
1326
- catch (error) {
1327
- turnCallbackBufferRef.current?.discard();
1328
- turnCallbackBufferRef.current = null;
1329
- if (signal?.aborted)
1330
- throw new provider_attempt_1.ProviderAttemptAbortError();
1331
- throw error;
1420
+ const result = attempt.value;
1421
+ providerIterations += 1;
1422
+ if (providerIterations === exports.MAX_PROVIDER_ITERATIONS && result.toolCalls.length > 0) {
1423
+ throw new Error(`provider iteration limit exhausted at response ${exports.MAX_PROVIDER_ITERATIONS} before tool execution`);
1332
1424
  }
1333
- };
1334
- const callProviderTurnWithOverflowRecovery = async () => {
1335
- try {
1336
- return await callProviderTurn();
1425
+ const streamCallbackBuffer = turnCallbackBufferRef.current;
1426
+ turnCallbackBufferRef.current = null;
1427
+ if (result.settleFinalization && !result.settleFinalization.ok) {
1428
+ // A completed settle payload is terminal provider output, not a prompt
1429
+ // for another model turn. Retractable callbacks have already cleared;
1430
+ // core-owned callback buffers must be discarded before surfacing the exact
1431
+ // parser failure through the ordinary terminal-error path.
1432
+ streamCallbackBuffer?.discard();
1433
+ finishTerminalProviderError(new Error(result.settleFinalization.errorCode), "unknown");
1434
+ continue;
1337
1435
  }
1338
- catch (error) {
1339
- if (error instanceof provider_attempt_1.ProviderAttemptAbortError)
1340
- throw error;
1341
- if (isContextOverflow(error) && !overflowRetried) {
1342
- overflowRetried = true;
1343
- stripLastToolCalls(messages);
1344
- stripLastToolCalls(generatedMessages);
1345
- const { maxTokens, contextMargin } = (0, config_1.getContextConfig)();
1346
- const trimmed = (0, context_1.trimMessages)(messages, maxTokens, contextMargin, maxTokens * 2);
1347
- const requiredEvidence = options?.requiredPromptEvidence;
1348
- const requiredMessages = new Set([
1349
- ...(requiredEvidence?.verifiedPredecessorMessage ? [requiredEvidence.verifiedPredecessorMessage] : []),
1350
- ...(requiredEvidence?.currentUserMessage ? [requiredEvidence.currentUserMessage] : []),
1351
- ]);
1352
- const trimmedMessages = new Set(trimmed);
1353
- const overflowRetryMessages = requiredMessages.size === 0
1354
- ? trimmed
1355
- : messages.filter((message) => trimmedMessages.has(message) || requiredMessages.has(message));
1356
- messages.splice(0, messages.length, ...overflowRetryMessages);
1357
- providerRuntime.resetTurnState(messages);
1358
- callbacks.onError(new Error("context trimmed, retrying..."), "transient");
1359
- return callProviderTurn();
1436
+ // Track usage from the latest API call
1437
+ if (result.usage)
1438
+ lastUsage = result.usage;
1439
+ // SHARED: build CC-format assistant message from TurnResult
1440
+ const msg = {
1441
+ role: "assistant",
1442
+ };
1443
+ // Persist assistant content WITHOUT inline <think>...</think> blocks.
1444
+ // Reasoning content already routed through onReasoningChunk for live
1445
+ // surfacing and persisted separately as `_reasoning_items` for
1446
+ // providers that support a reasoning channel; saving it inline AND
1447
+ // alongside tool_calls causes MiniMax to reject the replayed turn
1448
+ // with "tool result's tool id not found" (error code 2013) because
1449
+ // it can't reconcile reasoning-with-tools in the same assistant
1450
+ // message. Strip aggressively at persist so the next replay is
1451
+ // clean; preserve the original reasoning trace on the message via
1452
+ // `_inline_reasoning` so debug/audit paths can still see it.
1453
+ if (result.content) {
1454
+ const stripped = stripThinkBlocksForViolationCheck(result.content);
1455
+ if (stripped.length > 0)
1456
+ msg.content = stripped;
1457
+ if (stripped.length !== result.content.length) {
1458
+ msg._inline_reasoning = result.content;
1459
+ (0, runtime_1.emitNervesEvent)({
1460
+ level: "info",
1461
+ component: "engine",
1462
+ event: "engine.inline_reasoning_stripped",
1463
+ message: "stripped inline <think> blocks from persisted assistant message; preserved on _inline_reasoning",
1464
+ meta: {
1465
+ provider: providerRuntime.id,
1466
+ model: providerRuntime.model,
1467
+ originalLength: result.content.length,
1468
+ strippedLength: stripped.length,
1469
+ },
1470
+ });
1360
1471
  }
1361
- throw error;
1362
1472
  }
1363
- };
1364
- const attempt = await (0, provider_attempt_1.runProviderAttempt)({
1365
- operation: "turn",
1366
- provider: providerRuntime.id,
1367
- model: providerRuntime.model,
1368
- run: callProviderTurnWithOverflowRecovery,
1369
- classifyError: (error) => providerRuntime.classifyError(error),
1370
- onRetry: async (record, maxAttempts) => {
1371
- const delayMs = record.delayMs;
1372
- const seconds = delayMs / 1000;
1373
- const cause = RETRY_LABELS[record.classification];
1374
- try {
1375
- if (record.provider === "openai-codex" && record.classification === "auth-failure") {
1376
- await (0, openai_codex_token_1.refreshOpenAICodexProviderCredentials)((0, identity_2.getAgentName)(), {
1377
- force: true,
1378
- reason: "turn-auth-failure",
1473
+ if (result.toolCalls.length)
1474
+ msg.tool_calls = result.toolCalls.map((tc) => ({
1475
+ id: tc.id,
1476
+ type: "function",
1477
+ function: { name: tc.name, arguments: tc.arguments },
1478
+ }));
1479
+ // Store reasoning items from the API response on the assistant message
1480
+ // so they persist through session save/load and can be restored in toResponsesInput
1481
+ const reasoningItems = result.outputItems.filter((item) => "type" in item && item.type === "reasoning");
1482
+ if (reasoningItems.length > 0) {
1483
+ msg._reasoning_items = reasoningItems;
1484
+ }
1485
+ // Store thinking blocks (Anthropic) on the assistant message for round-tripping
1486
+ const thinkingItems = result.outputItems.filter((item) => "type" in item && (item.type === "thinking" || item.type === "redacted_thinking"));
1487
+ if (thinkingItems.length > 0) {
1488
+ msg._thinking_blocks = thinkingItems;
1489
+ }
1490
+ // Phase annotation for Codex provider
1491
+ const hasPhaseAnnotation = providerRuntime.capabilities.has("phase-annotation");
1492
+ const isSoleSettle = result.toolCalls.length === 1 && result.toolCalls[0].name === "settle";
1493
+ if (hasPhaseAnnotation) {
1494
+ msg.phase = isSoleSettle ? "settle" : "commentary";
1495
+ }
1496
+ // Detect the MiniMax "only-thinking, no tool call" violation: no tool
1497
+ // calls returned, and the content is empty after stripping
1498
+ // <think>...</think> blocks. This is a narrow check — legitimate
1499
+ // content-only responses (text without think tags, or text outside
1500
+ // think tags) still flow through the original "no tool calls →
1501
+ // accept as-is" path so existing channels and tests are unaffected.
1502
+ const onlyThinkContent = !result.toolCalls.length
1503
+ && typeof result.content === "string"
1504
+ && stripThinkBlocksForViolationCheck(result.content).length === 0
1505
+ && result.content.length > 0;
1506
+ const privateReturnTextAckRetryError = !result.toolCalls.length
1507
+ ? privateReturnMissingPonderError({
1508
+ latestUserRequest: latestUserMessageText(messages),
1509
+ answer: stripThinkBlocksForViolationCheck(result.content),
1510
+ sawPonder,
1511
+ })
1512
+ : null;
1513
+ if (!result.toolCalls.length) {
1514
+ if (forcingHistoricalEffect) {
1515
+ streamCallbackBuffer?.discard();
1516
+ callbacks.onClearText?.();
1517
+ const retryableHistoricalTools = [...forcedHistoricalToolNames];
1518
+ if (historicalToolFailureRetries >= NO_TOOL_CALL_MAX_RETRIES) {
1519
+ const blockedAnswer = `I could not complete the unresolved ${retryableHistoricalTools.join(", ")} effect because the provider repeatedly returned no tool call. I did not treat the requested change as complete.`;
1520
+ (0, runtime_1.emitNervesEvent)({
1521
+ level: "warn",
1522
+ component: "engine",
1523
+ event: "engine.historical_tool_failure_retry",
1524
+ message: "unresolved historical effect exhausted deterministic tool-call retries; failing closed",
1525
+ meta: { provider: providerRuntime.id, model: providerRuntime.model, toolNames: retryableHistoricalTools, cap: NO_TOOL_CALL_MAX_RETRIES },
1379
1526
  });
1527
+ msg.content = blockedAnswer;
1528
+ pushGenerated(msg);
1529
+ callbacks.onTextChunk(blockedAnswer);
1530
+ completion = { answer: blockedAnswer, intent: "blocked" };
1531
+ outcome = "blocked";
1532
+ done = true;
1533
+ continue;
1380
1534
  }
1381
- await (0, provider_credentials_1.refreshProviderCredentialPool)((0, identity_2.getAgentName)(), {
1382
- preserveCachedOnFailure: true,
1383
- providers: [record.provider],
1535
+ historicalToolFailureRetries += 1;
1536
+ (0, runtime_1.emitNervesEvent)({
1537
+ level: "warn",
1538
+ component: "engine",
1539
+ event: "engine.historical_tool_failure_retry",
1540
+ message: "unresolved historical effect returned no tool call; forcing another exact-effect attempt",
1541
+ meta: {
1542
+ provider: providerRuntime.id,
1543
+ model: providerRuntime.model,
1544
+ toolNames: retryableHistoricalTools,
1545
+ attempt: historicalToolFailureRetries,
1546
+ cap: NO_TOOL_CALL_MAX_RETRIES,
1547
+ },
1384
1548
  });
1385
- _providerRuntimeFactories[facing] = null;
1386
- providerRuntime = await getProviderRuntime(facing);
1387
- providerRuntime.resetTurnState(messages);
1549
+ pushGenerated(msg);
1550
+ messages.push({
1551
+ role: "user",
1552
+ content: `this exact request previously reached a tool that is advertised again now. Its unresolved historical effect is through ${retryableHistoricalTools.join(", ")}. Only that tool is available now. Read current state if needed, then retry the exact failed effect. Do not report completion until the effect succeeds.`,
1553
+ });
1554
+ continue;
1388
1555
  }
1389
- catch (refreshError) {
1556
+ const requiredToolCallsGate = pendingRequiredToolCalls();
1557
+ if (requiredToolCallsGate) {
1558
+ streamCallbackBuffer?.discard();
1559
+ callbacks.onClearText?.();
1560
+ queueRequiredCorrection(requiredToolCallsGate.message, "before required tool calls completed");
1390
1561
  (0, runtime_1.emitNervesEvent)({
1391
1562
  level: "warn",
1392
1563
  component: "engine",
1393
- event: "engine.provider_retry_refresh_failed",
1394
- message: "provider credential refresh failed during retry",
1395
- meta: { provider: record.provider, model: record.model, reason: refreshError instanceof Error ? refreshError.message : String(refreshError) },
1564
+ event: "engine.required_tool_calls_pending",
1565
+ message: "terminal response rejected until required tool handlers dispatch",
1566
+ meta: { missingToolNames: requiredToolCallsGate.missing },
1396
1567
  });
1568
+ continue;
1397
1569
  }
1398
- callbacks.onError(new Error(`${cause}, retrying in ${seconds}s (${record.attempt}/${maxAttempts})...`), "transient");
1399
- },
1400
- sleep: async (delayMs) => {
1401
- await waitForProviderRetry(delayMs, signal);
1402
- providerRuntime.resetTurnState(messages);
1403
- },
1404
- });
1405
- if (!attempt.ok) {
1406
- finishTerminalProviderError(attempt.error, attempt.classification);
1407
- continue;
1408
- }
1409
- const result = attempt.value;
1410
- providerIterations += 1;
1411
- if (providerIterations === exports.MAX_PROVIDER_ITERATIONS && result.toolCalls.length > 0) {
1412
- throw new Error(`provider iteration limit exhausted at response ${exports.MAX_PROVIDER_ITERATIONS} before tool execution`);
1413
- }
1414
- const streamCallbackBuffer = turnCallbackBufferRef.current;
1415
- turnCallbackBufferRef.current = null;
1416
- if (result.settleFinalization && !result.settleFinalization.ok) {
1417
- // A completed settle payload is terminal provider output, not a prompt
1418
- // for another model turn. Retractable callbacks have already cleared;
1419
- // core-owned callback buffers must be discarded before surfacing the exact
1420
- // parser failure through the ordinary terminal-error path.
1421
- streamCallbackBuffer?.discard();
1422
- finishTerminalProviderError(new Error(result.settleFinalization.errorCode), "unknown");
1423
- continue;
1424
- }
1425
- // Track usage from the latest API call
1426
- if (result.usage)
1427
- lastUsage = result.usage;
1428
- // SHARED: build CC-format assistant message from TurnResult
1429
- const msg = {
1430
- role: "assistant",
1431
- };
1432
- // Persist assistant content WITHOUT inline <think>...</think> blocks.
1433
- // Reasoning content already routed through onReasoningChunk for live
1434
- // surfacing and persisted separately as `_reasoning_items` for
1435
- // providers that support a reasoning channel; saving it inline AND
1436
- // alongside tool_calls causes MiniMax to reject the replayed turn
1437
- // with "tool result's tool id not found" (error code 2013) because
1438
- // it can't reconcile reasoning-with-tools in the same assistant
1439
- // message. Strip aggressively at persist so the next replay is
1440
- // clean; preserve the original reasoning trace on the message via
1441
- // `_inline_reasoning` so debug/audit paths can still see it.
1442
- if (result.content) {
1443
- const stripped = stripThinkBlocksForViolationCheck(result.content);
1444
- if (stripped.length > 0)
1445
- msg.content = stripped;
1446
- if (stripped.length !== result.content.length) {
1447
- msg._inline_reasoning = result.content;
1448
- (0, runtime_1.emitNervesEvent)({
1449
- level: "info",
1450
- component: "engine",
1451
- event: "engine.inline_reasoning_stripped",
1452
- message: "stripped inline <think> blocks from persisted assistant message; preserved on _inline_reasoning",
1453
- meta: {
1454
- provider: providerRuntime.id,
1455
- model: providerRuntime.model,
1456
- originalLength: result.content.length,
1457
- strippedLength: stripped.length,
1458
- },
1459
- });
1460
- }
1461
- }
1462
- if (result.toolCalls.length)
1463
- msg.tool_calls = result.toolCalls.map((tc) => ({
1464
- id: tc.id,
1465
- type: "function",
1466
- function: { name: tc.name, arguments: tc.arguments },
1467
- }));
1468
- // Store reasoning items from the API response on the assistant message
1469
- // so they persist through session save/load and can be restored in toResponsesInput
1470
- const reasoningItems = result.outputItems.filter((item) => "type" in item && item.type === "reasoning");
1471
- if (reasoningItems.length > 0) {
1472
- msg._reasoning_items = reasoningItems;
1473
- }
1474
- // Store thinking blocks (Anthropic) on the assistant message for round-tripping
1475
- const thinkingItems = result.outputItems.filter((item) => "type" in item && (item.type === "thinking" || item.type === "redacted_thinking"));
1476
- if (thinkingItems.length > 0) {
1477
- msg._thinking_blocks = thinkingItems;
1478
- }
1479
- // Phase annotation for Codex provider
1480
- const hasPhaseAnnotation = providerRuntime.capabilities.has("phase-annotation");
1481
- const isSoleSettle = result.toolCalls.length === 1 && result.toolCalls[0].name === "settle";
1482
- if (hasPhaseAnnotation) {
1483
- msg.phase = isSoleSettle ? "settle" : "commentary";
1484
- }
1485
- // Detect the MiniMax "only-thinking, no tool call" violation: no tool
1486
- // calls returned, and the content is empty after stripping
1487
- // <think>...</think> blocks. This is a narrow check — legitimate
1488
- // content-only responses (text without think tags, or text outside
1489
- // think tags) still flow through the original "no tool calls →
1490
- // accept as-is" path so existing channels and tests are unaffected.
1491
- const onlyThinkContent = !result.toolCalls.length
1492
- && typeof result.content === "string"
1493
- && stripThinkBlocksForViolationCheck(result.content).length === 0
1494
- && result.content.length > 0;
1495
- const privateReturnTextAckRetryError = !result.toolCalls.length
1496
- ? privateReturnMissingPonderError({
1497
- latestUserRequest: latestUserMessageText(messages),
1498
- answer: stripThinkBlocksForViolationCheck(result.content),
1499
- sawPonder,
1500
- })
1501
- : null;
1502
- if (!result.toolCalls.length) {
1503
- if (forcingHistoricalEffect) {
1504
- streamCallbackBuffer?.discard();
1505
- callbacks.onClearText?.();
1506
- const retryableHistoricalTools = [...forcedHistoricalToolNames];
1507
- if (historicalToolFailureRetries >= NO_TOOL_CALL_MAX_RETRIES) {
1508
- const blockedAnswer = `I could not complete the unresolved ${retryableHistoricalTools.join(", ")} effect because the provider repeatedly returned no tool call. I did not treat the requested change as complete.`;
1570
+ const requiredAnswerRejection = options?.requiredToolCalls?.validateTerminalAnswer?.(String(msg.content ?? ""));
1571
+ if (requiredAnswerRejection) {
1572
+ streamCallbackBuffer?.discard();
1573
+ callbacks.onClearText?.();
1574
+ queueRequiredCorrection(requiredAnswerRejection, "before required terminal answer validation completed");
1575
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "engine", event: "engine.required_tool_answer_rejected", message: "unsupported terminal answer rejected after required reads", meta: { answerLength: String(msg.content ?? "").length } });
1576
+ continue;
1577
+ }
1578
+ if (privateReturnTextAckRetryError) {
1579
+ streamCallbackBuffer?.discard();
1580
+ callbacks.onClearText?.();
1581
+ if (noToolCallRetries < NO_TOOL_CALL_MAX_RETRIES) {
1582
+ noToolCallRetries++;
1583
+ (0, runtime_1.emitNervesEvent)({
1584
+ level: "warn",
1585
+ component: "engine",
1586
+ event: "engine.no_tool_call_retry",
1587
+ message: "model returned a text-only private-return acknowledgement without ponder; retrying with corrective nudge",
1588
+ meta: {
1589
+ attempt: noToolCallRetries,
1590
+ cap: NO_TOOL_CALL_MAX_RETRIES,
1591
+ provider: providerRuntime.id,
1592
+ model: providerRuntime.model,
1593
+ reason: "private_return_missing_ponder",
1594
+ contentLength: result.content.length,
1595
+ },
1596
+ });
1597
+ pushGenerated(msg);
1598
+ messages.push({
1599
+ role: "user",
1600
+ content: `${privateReturnTextAckRetryError} Emit the ponder(action=create, ...) tool call now, or ask a blocking clarification without saying the private work is queued.`,
1601
+ });
1602
+ continue;
1603
+ }
1604
+ const blockedAnswer = "I could not start the private pass. No private-attention packet was created, so no return work was queued.";
1509
1605
  (0, runtime_1.emitNervesEvent)({
1510
- level: "warn",
1606
+ level: "error",
1511
1607
  component: "engine",
1512
- event: "engine.historical_tool_failure_retry",
1513
- message: "unresolved historical effect exhausted deterministic tool-call retries; failing closed",
1514
- meta: { provider: providerRuntime.id, model: providerRuntime.model, toolNames: retryableHistoricalTools, cap: NO_TOOL_CALL_MAX_RETRIES },
1608
+ event: "engine.private_return_missing_ponder_blocked",
1609
+ message: "private-return text acknowledgement skipped ponder through the retry cap; failing closed",
1610
+ meta: {
1611
+ cap: NO_TOOL_CALL_MAX_RETRIES,
1612
+ provider: providerRuntime.id,
1613
+ model: providerRuntime.model,
1614
+ contentLength: result.content.length,
1615
+ },
1515
1616
  });
1516
1617
  msg.content = blockedAnswer;
1517
1618
  pushGenerated(msg);
@@ -1521,895 +1622,809 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1521
1622
  done = true;
1522
1623
  continue;
1523
1624
  }
1524
- historicalToolFailureRetries += 1;
1525
- (0, runtime_1.emitNervesEvent)({
1526
- level: "warn",
1527
- component: "engine",
1528
- event: "engine.historical_tool_failure_retry",
1529
- message: "unresolved historical effect returned no tool call; forcing another exact-effect attempt",
1530
- meta: {
1531
- provider: providerRuntime.id,
1532
- model: providerRuntime.model,
1533
- toolNames: retryableHistoricalTools,
1534
- attempt: historicalToolFailureRetries,
1535
- cap: NO_TOOL_CALL_MAX_RETRIES,
1536
- },
1537
- });
1538
- pushGenerated(msg);
1539
- messages.push({
1540
- role: "user",
1541
- content: `this exact request previously reached a tool that is advertised again now. Its unresolved historical effect is through ${retryableHistoricalTools.join(", ")}. Only that tool is available now. Read current state if needed, then retry the exact failed effect. Do not report completion until the effect succeeds.`,
1542
- });
1543
- continue;
1544
- }
1545
- const requiredToolCallsGate = pendingRequiredToolCalls();
1546
- if (requiredToolCallsGate) {
1547
- streamCallbackBuffer?.discard();
1548
- callbacks.onClearText?.();
1549
- queueRequiredCorrection(requiredToolCallsGate.message, "before required tool calls completed");
1550
- (0, runtime_1.emitNervesEvent)({
1551
- level: "warn",
1552
- component: "engine",
1553
- event: "engine.required_tool_calls_pending",
1554
- message: "terminal response rejected until required tool handlers dispatch",
1555
- meta: { missingToolNames: requiredToolCallsGate.missing },
1556
- });
1557
- continue;
1558
- }
1559
- const requiredAnswerRejection = options?.requiredToolCalls?.validateTerminalAnswer?.(String(msg.content ?? ""));
1560
- if (requiredAnswerRejection) {
1561
- streamCallbackBuffer?.discard();
1562
- callbacks.onClearText?.();
1563
- queueRequiredCorrection(requiredAnswerRejection, "before required terminal answer validation completed");
1564
- (0, runtime_1.emitNervesEvent)({ level: "warn", component: "engine", event: "engine.required_tool_answer_rejected", message: "unsupported terminal answer rejected after required reads", meta: { answerLength: String(msg.content ?? "").length } });
1565
- continue;
1566
- }
1567
- if (privateReturnTextAckRetryError) {
1568
- streamCallbackBuffer?.discard();
1569
- callbacks.onClearText?.();
1570
- if (noToolCallRetries < NO_TOOL_CALL_MAX_RETRIES) {
1625
+ if (onlyThinkContent && toolChoiceRequired && noToolCallRetries < NO_TOOL_CALL_MAX_RETRIES) {
1626
+ streamCallbackBuffer?.discard();
1627
+ // Provider-level violation: tool_choice was required, model emitted
1628
+ // only a <think>...</think> block (or empty content) with no tool
1629
+ // call. Retry with a corrective nudge up to NO_TOOL_CALL_MAX_RETRIES
1630
+ // times. After cap, accept as-is (the readback path strips think
1631
+ // tags and surfaces a clear diagnostic).
1571
1632
  noToolCallRetries++;
1572
1633
  (0, runtime_1.emitNervesEvent)({
1573
1634
  level: "warn",
1574
1635
  component: "engine",
1575
1636
  event: "engine.no_tool_call_retry",
1576
- message: "model returned a text-only private-return acknowledgement without ponder; retrying with corrective nudge",
1637
+ message: "model returned only <think> content with no tool call despite tool_choice=required; retrying with corrective nudge",
1577
1638
  meta: {
1578
1639
  attempt: noToolCallRetries,
1579
1640
  cap: NO_TOOL_CALL_MAX_RETRIES,
1580
1641
  provider: providerRuntime.id,
1581
1642
  model: providerRuntime.model,
1582
- reason: "private_return_missing_ponder",
1583
1643
  contentLength: result.content.length,
1584
1644
  },
1585
1645
  });
1586
1646
  pushGenerated(msg);
1587
1647
  messages.push({
1588
1648
  role: "user",
1589
- content: `${privateReturnTextAckRetryError} Emit the ponder(action=create, ...) tool call now, or ask a blocking clarification without saying the private work is queued.`,
1649
+ content: isPrivateRuntimeChannel
1650
+ ? augmentedToolContext?.noSend === true
1651
+ ? "no tool was called this turn. this is an immutable no-send turn; call rest now without creating a continuation."
1652
+ : "no tool was called this turn. you must end every turn by calling rest (or surface, ponder, observe). emit the tool call now."
1653
+ : "no tool was called this turn. you must end every turn by calling settle with your answer (or ponder/observe). emit the tool call now.",
1590
1654
  });
1591
1655
  continue;
1592
1656
  }
1593
- const blockedAnswer = "I could not start the private pass. No private-attention packet was created, so no return work was queued.";
1594
- (0, runtime_1.emitNervesEvent)({
1595
- level: "error",
1596
- component: "engine",
1597
- event: "engine.private_return_missing_ponder_blocked",
1598
- message: "private-return text acknowledgement skipped ponder through the retry cap; failing closed",
1599
- meta: {
1600
- cap: NO_TOOL_CALL_MAX_RETRIES,
1601
- provider: providerRuntime.id,
1602
- model: providerRuntime.model,
1603
- contentLength: result.content.length,
1604
- },
1605
- });
1606
- msg.content = blockedAnswer;
1607
- pushGenerated(msg);
1608
- callbacks.onTextChunk(blockedAnswer);
1609
- completion = { answer: blockedAnswer, intent: "blocked" };
1610
- outcome = "blocked";
1611
- done = true;
1612
- continue;
1613
- }
1614
- if (onlyThinkContent && toolChoiceRequired && noToolCallRetries < NO_TOOL_CALL_MAX_RETRIES) {
1615
- streamCallbackBuffer?.discard();
1616
- // Provider-level violation: tool_choice was required, model emitted
1617
- // only a <think>...</think> block (or empty content) with no tool
1618
- // call. Retry with a corrective nudge up to NO_TOOL_CALL_MAX_RETRIES
1619
- // times. After cap, accept as-is (the readback path strips think
1620
- // tags and surfaces a clear diagnostic).
1621
- noToolCallRetries++;
1622
- (0, runtime_1.emitNervesEvent)({
1623
- level: "warn",
1624
- component: "engine",
1625
- event: "engine.no_tool_call_retry",
1626
- message: "model returned only <think> content with no tool call despite tool_choice=required; retrying with corrective nudge",
1627
- meta: {
1628
- attempt: noToolCallRetries,
1629
- cap: NO_TOOL_CALL_MAX_RETRIES,
1630
- provider: providerRuntime.id,
1631
- model: providerRuntime.model,
1632
- contentLength: result.content.length,
1633
- },
1634
- });
1635
- pushGenerated(msg);
1636
- messages.push({
1637
- role: "user",
1638
- content: isPrivateRuntimeChannel
1639
- ? augmentedToolContext?.noSend === true
1640
- ? "no tool was called this turn. this is an immutable no-send turn; call rest now without creating a continuation."
1641
- : "no tool was called this turn. you must end every turn by calling rest (or surface, ponder, observe). emit the tool call now."
1642
- : "no tool was called this turn. you must end every turn by calling settle with your answer (or ponder/observe). emit the tool call now.",
1643
- });
1644
- continue;
1645
- }
1646
- // Legitimate text-only response, or cap reached — accept as-is.
1647
- await streamCallbackBuffer?.flush();
1648
- pushGenerated(msg);
1649
- done = true;
1650
- }
1651
- else {
1652
- // Reset the retry counter on any successful tool call.
1653
- noToolCallRetries = 0;
1654
- const preCallMessages = structuredClone(messages.filter((message) => message.role !== "system"));
1655
- const validatedCalls = validateToolCallBatchAtProductionBoundary(result.toolCalls, activeTools);
1656
- const invalidCall = validatedCalls.find((entry) => "error" in entry);
1657
- if (invalidCall) {
1657
+ // Legitimate text-only response, or cap reached — accept as-is.
1658
1658
  await streamCallbackBuffer?.flush();
1659
1659
  pushGenerated(msg);
1660
- const unadvertisedCall = validatedCalls.find((entry) => !activeToolNames.has(entry.call.name));
1661
- for (const entry of validatedCalls) {
1662
- const detail = "error" in entry ? entry.error : "another call in this batch had invalid arguments";
1663
- const rejection = unadvertisedCall
1664
- ? `rejected: ${entry.call.name} was not advertised for this channel; no handler was executed.`
1665
- : `invalid tool arguments: ${detail}`;
1666
- pushGenerated({ role: "tool", tool_call_id: entry.call.id, content: rejection });
1667
- providerRuntime.appendToolOutput(entry.call.id, rejection);
1668
- options?.toolBoundaryObserver?.({
1669
- name: entry.call.name,
1670
- reason: activeToolNames.has(entry.call.name) ? "invalid_arguments" : "profile_excluded",
1671
- globallyResolvable: typeof (0, tools_1.resolveToolDefinition)(entry.call.name)?.handler === "function",
1672
- invoked: false,
1673
- sideEffect: false,
1674
- });
1675
- }
1676
- if (unadvertisedCall) {
1677
- (0, runtime_1.emitNervesEvent)({
1678
- level: "warn",
1679
- component: "engine",
1680
- event: "engine.unadvertised_tool_blocked",
1681
- message: "blocked an unadvertised tool call before batch execution",
1682
- meta: { channel: String(channel), toolName: unadvertisedCall.call.name },
1683
- });
1684
- }
1685
- else {
1686
- (0, runtime_1.emitNervesEvent)({
1687
- level: "warn",
1688
- component: "engine",
1689
- event: "engine.tool_arguments_rejected",
1690
- message: "tool batch rejected before execution because arguments were invalid",
1691
- meta: { toolCallId: invalidCall.call.id, toolName: invalidCall.call.name },
1692
- });
1693
- }
1694
- continue;
1695
- }
1696
- const validCalls = validatedCalls;
1697
- const validatedCallArguments = new Map(validCalls.map((entry) => [
1698
- entry.call,
1699
- entry.validated.arguments,
1700
- ]));
1701
- const habitBlockReason = await habitToolBatchBlockReason(habitSession, result.toolCalls, augmentedToolContext?.delegatedOrigins);
1702
- if (habitBlockReason) {
1703
- streamCallbackBuffer?.discard();
1704
- recordBlockedHabitSurfaceAttempts(habitSession, result.toolCalls, habitBlockReason);
1705
- pushGenerated(msg);
1706
- const blockedOutput = `blocked: ${habitBlockReason}. No tool side effects from this assistant message were executed.`;
1707
- for (const call of result.toolCalls) {
1708
- pushGenerated({ role: "tool", tool_call_id: call.id, content: blockedOutput });
1709
- providerRuntime.appendToolOutput(call.id, blockedOutput);
1710
- }
1711
- (0, runtime_1.emitNervesEvent)({
1712
- level: "warn",
1713
- component: "engine",
1714
- event: "engine.habit_tool_batch_blocked",
1715
- message: "habit tool batch blocked before side effects",
1716
- meta: { reason: habitBlockReason, toolCalls: result.toolCalls.map((call) => call.name) },
1717
- });
1718
- continue;
1660
+ done = true;
1719
1661
  }
1720
- const soleTerminalCall = result.toolCalls.length === 1
1721
- ? result.toolCalls[0]
1722
- : null;
1723
- const soleTerminalProjection = soleTerminalCall
1724
- ? (0, tools_1.resolveToolDefinition)(soleTerminalCall.name)?.terminalProjection
1725
- : undefined;
1726
- if (soleTerminalCall && soleTerminalProjection?.mode === "verbatim") {
1727
- const terminalArgs = validatedCallArguments.get(soleTerminalCall);
1728
- if (soleTerminalProjection.clearBufferedText
1729
- || callbacks.settleOutputMode === "final_only") {
1730
- streamCallbackBuffer?.discard();
1731
- }
1732
- else {
1662
+ else {
1663
+ // Reset the retry counter on any successful tool call.
1664
+ noToolCallRetries = 0;
1665
+ const preCallMessages = structuredClone(messagesWithoutRequiredCorrections().filter((message) => message.role !== "system"));
1666
+ const validatedCalls = validateToolCallBatchAtProductionBoundary(result.toolCalls, activeTools);
1667
+ const invalidCall = validatedCalls.find((entry) => "error" in entry);
1668
+ if (invalidCall) {
1733
1669
  await streamCallbackBuffer?.flush();
1734
- }
1735
- if (soleTerminalProjection.clearBufferedText)
1736
- callbacks.onClearText?.();
1737
- callbacks.onToolStart(soleTerminalCall.name, terminalArgs);
1738
- let terminalResult;
1739
- try {
1740
- const execToolFn = options?.execTool ?? tools_1.execTool;
1741
- terminalResult = await execToolFn(soleTerminalCall.name, terminalArgs, augmentedToolContext);
1742
- }
1743
- catch (error) {
1744
- callbacks.onToolEnd(soleTerminalCall.name, (0, tools_1.summarizeArgs)(soleTerminalCall.name, terminalArgs), false);
1745
1670
  pushGenerated(msg);
1746
- const failure = error instanceof Error ? `error: ${error.message}` : `error: ${String(error)}`;
1747
- pushGenerated({ role: "tool", tool_call_id: soleTerminalCall.id, content: failure });
1748
- providerRuntime.appendToolOutput(soleTerminalCall.id, failure);
1749
- callbacks.onTextChunk(failure);
1750
- completion = { answer: failure, intent: "blocked" };
1751
- outcome = "blocked";
1752
- done = true;
1671
+ const unadvertisedCall = validatedCalls.find((entry) => !activeToolNames.has(entry.call.name));
1672
+ for (const entry of validatedCalls) {
1673
+ const detail = "error" in entry ? entry.error : "another call in this batch had invalid arguments";
1674
+ const rejection = unadvertisedCall
1675
+ ? `rejected: ${entry.call.name} was not advertised for this channel; no handler was executed.`
1676
+ : `invalid tool arguments: ${detail}`;
1677
+ pushGenerated({ role: "tool", tool_call_id: entry.call.id, content: rejection });
1678
+ providerRuntime.appendToolOutput(entry.call.id, rejection);
1679
+ options?.toolBoundaryObserver?.({
1680
+ name: entry.call.name,
1681
+ reason: activeToolNames.has(entry.call.name) ? "invalid_arguments" : "profile_excluded",
1682
+ globallyResolvable: typeof (0, tools_1.resolveToolDefinition)(entry.call.name)?.handler === "function",
1683
+ invoked: false,
1684
+ sideEffect: false,
1685
+ });
1686
+ }
1687
+ if (unadvertisedCall) {
1688
+ (0, runtime_1.emitNervesEvent)({
1689
+ level: "warn",
1690
+ component: "engine",
1691
+ event: "engine.unadvertised_tool_blocked",
1692
+ message: "blocked an unadvertised tool call before batch execution",
1693
+ meta: { channel: String(channel), toolName: unadvertisedCall.call.name },
1694
+ });
1695
+ }
1696
+ else {
1697
+ (0, runtime_1.emitNervesEvent)({
1698
+ level: "warn",
1699
+ component: "engine",
1700
+ event: "engine.tool_arguments_rejected",
1701
+ message: "tool batch rejected before execution because arguments were invalid",
1702
+ meta: { toolCallId: invalidCall.call.id, toolName: invalidCall.call.name },
1703
+ });
1704
+ }
1753
1705
  continue;
1754
1706
  }
1755
- callbacks.onToolEnd(soleTerminalCall.name, (0, tools_1.summarizeArgs)(soleTerminalCall.name, terminalArgs), true);
1756
- pushGenerated(msg);
1757
- pushGenerated({ role: "tool", tool_call_id: soleTerminalCall.id, content: terminalResult });
1758
- providerRuntime.appendToolOutput(soleTerminalCall.id, terminalResult);
1759
- callbacks.onTextChunk(terminalResult);
1760
- completion = { answer: terminalResult, intent: "complete" };
1761
- outcome = "settled";
1762
- done = true;
1763
- continue;
1764
- }
1765
- // Check for settle sole call: intercept before tool execution
1766
- if (isSoleSettle) {
1767
- const settleArgs = validatedCallArguments.get(result.toolCalls[0]);
1768
- callbacks.onToolStart("settle", settleArgs);
1769
- const requiredToolCallsGate = pendingRequiredToolCalls();
1770
- if (requiredToolCallsGate) {
1707
+ const validCalls = validatedCalls;
1708
+ const validatedCallArguments = new Map(validCalls.map((entry) => [
1709
+ entry.call,
1710
+ entry.validated.arguments,
1711
+ ]));
1712
+ const habitBlockReason = await habitToolBatchBlockReason(habitSession, result.toolCalls, augmentedToolContext?.delegatedOrigins);
1713
+ if (habitBlockReason) {
1771
1714
  streamCallbackBuffer?.discard();
1772
- callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
1773
- callbacks.onClearText?.();
1774
- queueRequiredCorrection(requiredToolCallsGate.message, "before required tool calls completed");
1715
+ recordBlockedHabitSurfaceAttempts(habitSession, result.toolCalls, habitBlockReason);
1716
+ pushGenerated(msg);
1717
+ const blockedOutput = `blocked: ${habitBlockReason}. No tool side effects from this assistant message were executed.`;
1718
+ for (const call of result.toolCalls) {
1719
+ pushGenerated({ role: "tool", tool_call_id: call.id, content: blockedOutput });
1720
+ providerRuntime.appendToolOutput(call.id, blockedOutput);
1721
+ }
1775
1722
  (0, runtime_1.emitNervesEvent)({
1776
1723
  level: "warn",
1777
1724
  component: "engine",
1778
- event: "engine.required_tool_calls_pending",
1779
- message: "settle rejected until required tool handlers dispatch",
1780
- meta: { missingToolNames: requiredToolCallsGate.missing },
1725
+ event: "engine.habit_tool_batch_blocked",
1726
+ message: "habit tool batch blocked before side effects",
1727
+ meta: { reason: habitBlockReason, toolCalls: result.toolCalls.map((call) => call.name) },
1781
1728
  });
1782
1729
  continue;
1783
1730
  }
1784
- // Private-runtime attention queue gate: reject settle if items remain
1785
- const attentionQueue = augmentedToolContext?.delegatedOrigins;
1786
- if (isPrivateRuntimeChannel && attentionQueue && attentionQueue.length > 0) {
1787
- streamCallbackBuffer?.discard();
1788
- callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
1789
- callbacks.onClearText?.();
1790
- pushGenerated(msg);
1791
- const gateMessage = "current held-work frame still has unsurfaced items — return each listed item with surface(delegationId=...) before you settle. Older transcript claims are historical; only the current held-work frame is the gate.";
1792
- pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: gateMessage });
1793
- providerRuntime.appendToolOutput(result.toolCalls[0].id, gateMessage);
1794
- continue;
1795
- }
1796
- // Extract answer from the tool call arguments.
1797
- // Supports: {"answer":"text","intent":"..."} or "text" (JSON string).
1798
- const { answer, intent } = parseSettlePayload(result.toolCalls[0].arguments);
1799
- const requiredAnswerRejection = options?.requiredToolCalls?.validateTerminalAnswer?.(answer);
1800
- if (requiredAnswerRejection) {
1801
- streamCallbackBuffer?.discard();
1802
- callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
1803
- callbacks.onClearText?.();
1804
- queueRequiredCorrection(requiredAnswerRejection, "before required terminal answer validation completed");
1805
- (0, runtime_1.emitNervesEvent)({ level: "warn", component: "engine", event: "engine.required_tool_answer_rejected", message: "unsupported settle answer rejected after required reads", meta: { answerLength: answer.length } });
1806
- continue;
1807
- }
1808
- // Private-runtime settle: no CompletionMetadata, "(settled)" ack
1809
- if (isPrivateRuntimeChannel) {
1810
- streamCallbackBuffer?.discard();
1811
- callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), true);
1731
+ const soleTerminalCall = result.toolCalls.length === 1
1732
+ ? result.toolCalls[0]
1733
+ : null;
1734
+ const soleTerminalProjection = soleTerminalCall
1735
+ ? (0, tools_1.resolveToolDefinition)(soleTerminalCall.name)?.terminalProjection
1736
+ : undefined;
1737
+ if (soleTerminalCall && soleTerminalProjection?.mode === "verbatim") {
1738
+ const terminalArgs = validatedCallArguments.get(soleTerminalCall);
1739
+ if (soleTerminalProjection.clearBufferedText
1740
+ || callbacks.settleOutputMode === "final_only") {
1741
+ streamCallbackBuffer?.discard();
1742
+ }
1743
+ else {
1744
+ await streamCallbackBuffer?.flush();
1745
+ }
1746
+ if (soleTerminalProjection.clearBufferedText)
1747
+ callbacks.onClearText?.();
1748
+ callbacks.onToolStart(soleTerminalCall.name, terminalArgs);
1749
+ let terminalResult;
1750
+ try {
1751
+ const execToolFn = options?.execTool ?? tools_1.execTool;
1752
+ terminalResult = await execToolFn(soleTerminalCall.name, terminalArgs, augmentedToolContext);
1753
+ }
1754
+ catch (error) {
1755
+ callbacks.onToolEnd(soleTerminalCall.name, (0, tools_1.summarizeArgs)(soleTerminalCall.name, terminalArgs), false);
1756
+ pushGenerated(msg);
1757
+ const failure = error instanceof Error ? `error: ${error.message}` : `error: ${String(error)}`;
1758
+ pushGenerated({ role: "tool", tool_call_id: soleTerminalCall.id, content: failure });
1759
+ providerRuntime.appendToolOutput(soleTerminalCall.id, failure);
1760
+ callbacks.onTextChunk(failure);
1761
+ completion = { answer: failure, intent: "blocked" };
1762
+ outcome = "blocked";
1763
+ done = true;
1764
+ continue;
1765
+ }
1766
+ callbacks.onToolEnd(soleTerminalCall.name, (0, tools_1.summarizeArgs)(soleTerminalCall.name, terminalArgs), true);
1812
1767
  pushGenerated(msg);
1813
- const settled = "(settled)";
1814
- pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: settled });
1815
- providerRuntime.appendToolOutput(result.toolCalls[0].id, settled);
1768
+ pushGenerated({ role: "tool", tool_call_id: soleTerminalCall.id, content: terminalResult });
1769
+ providerRuntime.appendToolOutput(soleTerminalCall.id, terminalResult);
1770
+ callbacks.onTextChunk(terminalResult);
1771
+ completion = { answer: terminalResult, intent: "complete" };
1816
1772
  outcome = "settled";
1817
1773
  done = true;
1818
1774
  continue;
1819
1775
  }
1820
- // The provider finalizer has already established a top-level string
1821
- // answer before ordinary settle handling reaches this point.
1822
- const deliveredAnswer = answer;
1823
- const retryError = privateReturnAckLeakError(deliveredAnswer, privateReturnHeldTokens)
1824
- ?? privateReturnMissingPonderError({
1825
- latestUserRequest: latestUserMessageText(messages),
1826
- answer: deliveredAnswer,
1827
- sawPonder,
1828
- })
1829
- ?? getSettleRetryError(mustResolveBeforeHandoffActive, intent, sawSteeringFollowUp, options?.delegationDecision, sawSendMessageSelf, sawPonder, sawQuerySession, options?.currentObligation ?? null, options?.activeWorkFrame?.inner?.job, sawExternalStateQuery);
1830
- const validDirectReply = mustResolveBeforeHandoffActive && intent === "direct_reply" && sawSteeringFollowUp;
1831
- if (retryError === null) {
1832
- try {
1833
- if (!result.settleStreamed) {
1834
- const acceptedOutputCallbacks = streamCallbackBuffer?.callbacks ?? callbacks;
1835
- acceptedOutputCallbacks.onTextChunk(deliveredAnswer);
1836
- }
1837
- await streamCallbackBuffer?.flush();
1776
+ // Check for settle sole call: intercept before tool execution
1777
+ if (isSoleSettle) {
1778
+ const settleArgs = validatedCallArguments.get(result.toolCalls[0]);
1779
+ callbacks.onToolStart("settle", settleArgs);
1780
+ const requiredToolCallsGate = pendingRequiredToolCalls();
1781
+ if (requiredToolCallsGate) {
1782
+ streamCallbackBuffer?.discard();
1783
+ callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
1784
+ callbacks.onClearText?.();
1785
+ queueRequiredCorrection(requiredToolCallsGate.message, "before required tool calls completed");
1786
+ (0, runtime_1.emitNervesEvent)({
1787
+ level: "warn",
1788
+ component: "engine",
1789
+ event: "engine.required_tool_calls_pending",
1790
+ message: "settle rejected until required tool handlers dispatch",
1791
+ meta: { missingToolNames: requiredToolCallsGate.missing },
1792
+ });
1793
+ continue;
1838
1794
  }
1839
- catch (error) {
1795
+ // Private-runtime attention queue gate: reject settle if items remain
1796
+ const attentionQueue = augmentedToolContext?.delegatedOrigins;
1797
+ if (isPrivateRuntimeChannel && attentionQueue && attentionQueue.length > 0) {
1798
+ streamCallbackBuffer?.discard();
1840
1799
  callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
1800
+ callbacks.onClearText?.();
1801
+ pushGenerated(msg);
1802
+ const gateMessage = "current held-work frame still has unsurfaced items — return each listed item with surface(delegationId=...) before you settle. Older transcript claims are historical; only the current held-work frame is the gate.";
1803
+ pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: gateMessage });
1804
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, gateMessage);
1805
+ continue;
1806
+ }
1807
+ // Extract answer from the tool call arguments.
1808
+ // Supports: {"answer":"text","intent":"..."} or "text" (JSON string).
1809
+ const { answer, intent } = parseSettlePayload(result.toolCalls[0].arguments);
1810
+ const requiredAnswerRejection = options?.requiredToolCalls?.validateTerminalAnswer?.(answer);
1811
+ if (requiredAnswerRejection) {
1841
1812
  streamCallbackBuffer?.discard();
1842
- finishTerminalProviderError(new streaming_1.SettleFinalizationCallbackError(error), "unknown");
1813
+ callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
1814
+ callbacks.onClearText?.();
1815
+ queueRequiredCorrection(requiredAnswerRejection, "before required terminal answer validation completed");
1816
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "engine", event: "engine.required_tool_answer_rejected", message: "unsupported settle answer rejected after required reads", meta: { answerLength: answer.length } });
1843
1817
  continue;
1844
1818
  }
1845
- callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), true);
1846
- completion = {
1847
- answer: deliveredAnswer,
1848
- intent: validDirectReply ? "direct_reply" : intent === "blocked" ? "blocked" : "complete",
1849
- };
1850
- // Retractable owners already hold the validated answer. Final-only
1851
- // owners receive it here, after every semantic continuation gate.
1852
- pushGenerated(msg);
1853
- if (validDirectReply) {
1854
- const resumeWork = "direct reply delivered. resume the unresolved obligation now and keep working until you can finish or clearly report that you are blocked.";
1855
- pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: resumeWork });
1856
- providerRuntime.appendToolOutput(result.toolCalls[0].id, resumeWork);
1819
+ // Private-runtime settle: no CompletionMetadata, "(settled)" ack
1820
+ if (isPrivateRuntimeChannel) {
1821
+ streamCallbackBuffer?.discard();
1822
+ callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), true);
1823
+ pushGenerated(msg);
1824
+ const settled = "(settled)";
1825
+ pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: settled });
1826
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, settled);
1827
+ outcome = "settled";
1828
+ done = true;
1829
+ continue;
1830
+ }
1831
+ // The provider finalizer has already established a top-level string
1832
+ // answer before ordinary settle handling reaches this point.
1833
+ const deliveredAnswer = answer;
1834
+ const retryError = privateReturnAckLeakError(deliveredAnswer, privateReturnHeldTokens)
1835
+ ?? privateReturnMissingPonderError({
1836
+ latestUserRequest: latestUserMessageText(messages),
1837
+ answer: deliveredAnswer,
1838
+ sawPonder,
1839
+ })
1840
+ ?? getSettleRetryError(mustResolveBeforeHandoffActive, intent, sawSteeringFollowUp, options?.delegationDecision, sawSendMessageSelf, sawPonder, sawQuerySession, options?.currentObligation ?? null, options?.activeWorkFrame?.inner?.job, sawExternalStateQuery);
1841
+ const validDirectReply = mustResolveBeforeHandoffActive && intent === "direct_reply" && sawSteeringFollowUp;
1842
+ if (retryError === null) {
1843
+ try {
1844
+ if (!result.settleStreamed) {
1845
+ const acceptedOutputCallbacks = streamCallbackBuffer?.callbacks ?? callbacks;
1846
+ acceptedOutputCallbacks.onTextChunk(deliveredAnswer);
1847
+ }
1848
+ await streamCallbackBuffer?.flush();
1849
+ }
1850
+ catch (error) {
1851
+ callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
1852
+ streamCallbackBuffer?.discard();
1853
+ finishTerminalProviderError(new streaming_1.SettleFinalizationCallbackError(error), "unknown");
1854
+ continue;
1855
+ }
1856
+ callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), true);
1857
+ completion = {
1858
+ answer: deliveredAnswer,
1859
+ intent: validDirectReply ? "direct_reply" : intent === "blocked" ? "blocked" : "complete",
1860
+ };
1861
+ // Retractable owners already hold the validated answer. Final-only
1862
+ // owners receive it here, after every semantic continuation gate.
1863
+ pushGenerated(msg);
1864
+ if (validDirectReply) {
1865
+ const resumeWork = "direct reply delivered. resume the unresolved obligation now and keep working until you can finish or clearly report that you are blocked.";
1866
+ pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: resumeWork });
1867
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, resumeWork);
1868
+ }
1869
+ else {
1870
+ const delivered = "(delivered)";
1871
+ pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: delivered });
1872
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, delivered);
1873
+ outcome = intent === "blocked" ? "blocked" : "settled";
1874
+ done = true;
1875
+ }
1857
1876
  }
1858
1877
  else {
1859
- const delivered = "(delivered)";
1860
- pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: delivered });
1861
- providerRuntime.appendToolOutput(result.toolCalls[0].id, delivered);
1862
- outcome = intent === "blocked" ? "blocked" : "settled";
1863
- done = true;
1878
+ // The payload is structurally final, but a semantic continuation
1879
+ // gate rejected it. Return that exact gate reason to the model.
1880
+ streamCallbackBuffer?.discard();
1881
+ callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
1882
+ callbacks.onClearText?.();
1883
+ pushGenerated(msg);
1884
+ const toolRetryMessage = retryError;
1885
+ pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: toolRetryMessage });
1886
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, toolRetryMessage);
1864
1887
  }
1888
+ continue;
1865
1889
  }
1866
- else {
1867
- // The payload is structurally final, but a semantic continuation
1868
- // gate rejected it. Return that exact gate reason to the model.
1890
+ // Check for observe sole call: intercept before tool execution
1891
+ const isSoleObserve = result.toolCalls.length === 1 && result.toolCalls[0].name === "observe";
1892
+ if (isSoleObserve) {
1869
1893
  streamCallbackBuffer?.discard();
1870
- callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
1871
- callbacks.onClearText?.();
1872
- pushGenerated(msg);
1873
- const toolRetryMessage = retryError;
1874
- pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: toolRetryMessage });
1875
- providerRuntime.appendToolOutput(result.toolCalls[0].id, toolRetryMessage);
1876
- }
1877
- continue;
1878
- }
1879
- // Check for observe sole call: intercept before tool execution
1880
- const isSoleObserve = result.toolCalls.length === 1 && result.toolCalls[0].name === "observe";
1881
- if (isSoleObserve) {
1882
- streamCallbackBuffer?.discard();
1883
- const observeArgs = validatedCallArguments.get(result.toolCalls[0]);
1884
- let reason;
1885
- if (typeof observeArgs?.reason === "string")
1886
- reason = observeArgs.reason;
1887
- callbacks.onToolStart("observe", observeArgs);
1888
- (0, runtime_1.emitNervesEvent)({
1889
- component: "engine",
1890
- event: "engine.observe",
1891
- message: "agent observed without responding",
1892
- meta: { ...(reason ? { reason } : {}) },
1893
- });
1894
- callbacks.onToolEnd("observe", (0, tools_1.summarizeArgs)("observe", observeArgs), true);
1895
- pushGenerated(msg);
1896
- const silenced = "(silenced)";
1897
- pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: silenced });
1898
- providerRuntime.appendToolOutput(result.toolCalls[0].id, silenced);
1899
- outcome = "observed";
1900
- done = true;
1901
- continue;
1902
- }
1903
- // Check for rest sole call: intercept before tool execution
1904
- const isSoleRest = result.toolCalls.length === 1 && result.toolCalls[0].name === "rest";
1905
- if (isSoleRest) {
1906
- streamCallbackBuffer?.discard();
1907
- const restArgs = validatedCallArguments.get(result.toolCalls[0]);
1908
- callbacks.onToolStart("rest", restArgs);
1909
- // Attention queue gate: reject rest if items remain
1910
- const attentionQueue = augmentedToolContext?.delegatedOrigins;
1911
- if (attentionQueue && attentionQueue.length > 0) {
1912
- callbacks.onToolEnd("rest", (0, tools_1.summarizeArgs)("rest", restArgs), false);
1894
+ const observeArgs = validatedCallArguments.get(result.toolCalls[0]);
1895
+ let reason;
1896
+ if (typeof observeArgs?.reason === "string")
1897
+ reason = observeArgs.reason;
1898
+ callbacks.onToolStart("observe", observeArgs);
1899
+ (0, runtime_1.emitNervesEvent)({
1900
+ component: "engine",
1901
+ event: "engine.observe",
1902
+ message: "agent observed without responding",
1903
+ meta: { ...(reason ? { reason } : {}) },
1904
+ });
1905
+ callbacks.onToolEnd("observe", (0, tools_1.summarizeArgs)("observe", observeArgs), true);
1913
1906
  pushGenerated(msg);
1914
- const gateMessage = "current held-work frame still has unsurfaced items — return each listed item with surface(delegationId=...) before you rest. Older transcript claims are historical; only the current held-work frame is the gate.";
1915
- pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: gateMessage });
1916
- providerRuntime.appendToolOutput(result.toolCalls[0].id, gateMessage);
1907
+ const silenced = "(silenced)";
1908
+ pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: silenced });
1909
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, silenced);
1910
+ outcome = "observed";
1911
+ done = true;
1917
1912
  continue;
1918
1913
  }
1919
- if (hasFreshPendingWork(options) && !freshWorkGateFired) {
1920
- freshWorkGateFired = true;
1921
- callbacks.onToolEnd("rest", (0, tools_1.summarizeArgs)("rest", restArgs), false);
1914
+ // Check for rest sole call: intercept before tool execution
1915
+ const isSoleRest = result.toolCalls.length === 1 && result.toolCalls[0].name === "rest";
1916
+ if (isSoleRest) {
1917
+ streamCallbackBuffer?.discard();
1918
+ const restArgs = validatedCallArguments.get(result.toolCalls[0]);
1919
+ callbacks.onToolStart("rest", restArgs);
1920
+ // Attention queue gate: reject rest if items remain
1921
+ const attentionQueue = augmentedToolContext?.delegatedOrigins;
1922
+ if (attentionQueue && attentionQueue.length > 0) {
1923
+ callbacks.onToolEnd("rest", (0, tools_1.summarizeArgs)("rest", restArgs), false);
1924
+ pushGenerated(msg);
1925
+ const gateMessage = "current held-work frame still has unsurfaced items — return each listed item with surface(delegationId=...) before you rest. Older transcript claims are historical; only the current held-work frame is the gate.";
1926
+ pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: gateMessage });
1927
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, gateMessage);
1928
+ continue;
1929
+ }
1930
+ if (hasFreshPendingWork(options) && !freshWorkGateFired) {
1931
+ freshWorkGateFired = true;
1932
+ callbacks.onToolEnd("rest", (0, tools_1.summarizeArgs)("rest", restArgs), false);
1933
+ pushGenerated(msg);
1934
+ const gateMessage = "fresh work arrived for me this turn — inspect the pending messages above and take the next concrete action before you rest.";
1935
+ pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: gateMessage });
1936
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, gateMessage);
1937
+ (0, runtime_1.emitNervesEvent)({
1938
+ level: "info",
1939
+ component: "engine",
1940
+ event: "engine.fresh_work_gate_fired",
1941
+ message: "rest deferred once because pending work arrived this turn; agent has been notified",
1942
+ meta: { pendingCount: options.pendingMessages.length },
1943
+ });
1944
+ continue;
1945
+ }
1946
+ callbacks.onToolEnd("rest", (0, tools_1.summarizeArgs)("rest", restArgs), true);
1922
1947
  pushGenerated(msg);
1923
- const gateMessage = "fresh work arrived for me this turn — inspect the pending messages above and take the next concrete action before you rest.";
1924
- pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: gateMessage });
1925
- providerRuntime.appendToolOutput(result.toolCalls[0].id, gateMessage);
1948
+ const ack = "(resting)";
1949
+ pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: ack });
1950
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, ack);
1926
1951
  (0, runtime_1.emitNervesEvent)({
1927
- level: "info",
1928
1952
  component: "engine",
1929
- event: "engine.fresh_work_gate_fired",
1930
- message: "rest deferred once because pending work arrived this turn; agent has been notified",
1931
- meta: { pendingCount: options.pendingMessages.length },
1953
+ event: "engine.rested",
1954
+ message: "resting until next heartbeat",
1955
+ meta: { ...(typeof restArgs?.status === "string" ? { status: restArgs.status } : {}) },
1932
1956
  });
1957
+ outcome = "rested";
1958
+ done = true;
1933
1959
  continue;
1934
1960
  }
1935
- callbacks.onToolEnd("rest", (0, tools_1.summarizeArgs)("rest", restArgs), true);
1936
- pushGenerated(msg);
1937
- const ack = "(resting)";
1938
- pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: ack });
1939
- providerRuntime.appendToolOutput(result.toolCalls[0].id, ack);
1940
- (0, runtime_1.emitNervesEvent)({
1941
- component: "engine",
1942
- event: "engine.rested",
1943
- message: "resting until next heartbeat",
1944
- meta: { ...(typeof restArgs?.status === "string" ? { status: restArgs.status } : {}) },
1945
- });
1946
- outcome = "rested";
1947
- done = true;
1948
- continue;
1949
- }
1950
- const requiredDispatchRejections = new Map();
1951
- for (const entry of validCalls) {
1952
- const requiredArgs = entry.validated.arguments;
1953
- const rejection = options?.requiredToolCalls?.validateToolCallBeforeDispatch?.(entry.call.name, requiredArgs);
1954
- if (rejection)
1955
- requiredDispatchRejections.set(entry.call.id, { name: entry.call.name, args: requiredArgs, message: rejection });
1956
- }
1957
- const approvalCalls = await Promise.all(validCalls.filter((entry) => !requiredDispatchRejections.has(entry.call.id)).map(async (entry) => {
1958
- const classification = await (0, tools_1.classifyApprovalForInvocation)(entry.call.name, entry.validated.arguments, augmentedToolContext);
1959
- return {
1960
- ...entry,
1961
- ...classification,
1962
- };
1963
- }));
1964
- const protectedCall = approvalCalls.find((entry) => entry.policy.kind === "required");
1965
- if (protectedCall && result.toolCalls.length !== 1) {
1966
- streamCallbackBuffer?.discard();
1967
- pushGenerated(msg);
1968
- for (const call of result.toolCalls) {
1969
- const rejection = "rejected: approval-eligible tool must be the sole call; no call in this batch was executed.";
1970
- pushGenerated({ role: "tool", tool_call_id: call.id, content: rejection });
1971
- providerRuntime.appendToolOutput(call.id, rejection);
1961
+ const requiredDispatchRejections = new Map();
1962
+ for (const entry of validCalls) {
1963
+ const requiredArgs = entry.validated.arguments;
1964
+ const rejection = options?.requiredToolCalls?.validateToolCallBeforeDispatch?.(entry.call.name, requiredArgs);
1965
+ if (rejection)
1966
+ requiredDispatchRejections.set(entry.call.id, { name: entry.call.name, args: requiredArgs, message: rejection });
1972
1967
  }
1973
- (0, runtime_1.emitNervesEvent)({
1974
- level: "warn",
1975
- component: "engine",
1976
- event: "engine.approval_mixed_batch_rejected",
1977
- message: "protected tool batch rejected before every handler",
1978
- meta: { toolCallCount: result.toolCalls.length, protectedToolName: protectedCall.call.name },
1979
- });
1980
- continue;
1981
- }
1982
- if (protectedCall && protectedCall.policy.kind === "required") {
1983
- if (!options?.approvalCoordinator) {
1968
+ const approvalCalls = await Promise.all(validCalls.filter((entry) => !requiredDispatchRejections.has(entry.call.id)).map(async (entry) => {
1969
+ const classification = await (0, tools_1.classifyApprovalForInvocation)(entry.call.name, entry.validated.arguments, augmentedToolContext);
1970
+ return {
1971
+ ...entry,
1972
+ ...classification,
1973
+ };
1974
+ }));
1975
+ const protectedCall = approvalCalls.find((entry) => entry.policy.kind === "required");
1976
+ if (protectedCall && result.toolCalls.length !== 1) {
1984
1977
  streamCallbackBuffer?.discard();
1985
1978
  pushGenerated(msg);
1986
- const rejection = "rejected: this protected tool requires approval, but the approval coordinator is unavailable; the handler was not invoked.";
1987
- pushGenerated({ role: "tool", tool_call_id: protectedCall.call.id, content: rejection });
1988
- providerRuntime.appendToolOutput(protectedCall.call.id, rejection);
1979
+ for (const call of result.toolCalls) {
1980
+ const rejection = "rejected: approval-eligible tool must be the sole call; no call in this batch was executed.";
1981
+ pushGenerated({ role: "tool", tool_call_id: call.id, content: rejection });
1982
+ providerRuntime.appendToolOutput(call.id, rejection);
1983
+ }
1989
1984
  (0, runtime_1.emitNervesEvent)({
1990
1985
  level: "warn",
1991
1986
  component: "engine",
1992
- event: "engine.approval_coordinator_unavailable",
1993
- message: "protected tool failed closed before handler execution",
1994
- meta: { toolName: protectedCall.call.name, toolCallId: protectedCall.call.id },
1987
+ event: "engine.approval_mixed_batch_rejected",
1988
+ message: "protected tool batch rejected before every handler",
1989
+ meta: { toolCallCount: result.toolCalls.length, protectedToolName: protectedCall.call.name },
1995
1990
  });
1996
1991
  continue;
1997
1992
  }
1998
- streamCallbackBuffer?.discard();
1999
- pushGenerated(msg);
2000
- const toolDigest = (0, tool_arguments_1.digestJson)({
2001
- name: protectedCall.call.name,
2002
- schemaDigest: protectedCall.validated.schemaDigest,
2003
- policyId: protectedCall.policy.policyId,
2004
- });
2005
- const policyDigest = (0, tool_arguments_1.digestJson)({
2006
- policyId: protectedCall.policy.policyId,
2007
- actionClass: protectedCall.policy.actionClass,
2008
- classification: "required",
2009
- });
2010
- const committed = await options.approvalCoordinator.propose({
2011
- toolCall: structuredClone(msg.tool_calls[0]),
2012
- arguments: structuredClone(protectedCall.validated.arguments),
2013
- preCallMessages,
2014
- frozenAssistantMessage: structuredClone(msg),
2015
- schemaDigest: protectedCall.validated.schemaDigest,
2016
- toolDigest,
2017
- policyDigest,
2018
- policyId: protectedCall.policy.policyId,
2019
- actionClass: protectedCall.policy.actionClass,
2020
- });
2021
- suspension = {
2022
- approvalId: committed.approvalId,
2023
- toolCallId: protectedCall.call.id,
2024
- checkpointDigest: committed.checkpointDigest,
2025
- suspendedSessionRevision: committed.suspendedSessionRevision,
2026
- };
2027
- outcome = "suspended";
2028
- done = true;
2029
- (0, runtime_1.emitNervesEvent)({
2030
- component: "engine",
2031
- event: "engine.approval_turn_suspended",
2032
- message: "agent turn suspended before protected tool execution",
2033
- meta: { approvalId: committed.approvalId, toolCallId: protectedCall.call.id },
2034
- });
2035
- continue;
2036
- }
2037
- const containsSoleCallOnlyViolation = result.toolCalls.length > 1
2038
- && result.toolCalls.some((call) => {
2039
- const terminalProjection = (0, tools_1.resolveToolDefinition)(call.name)?.terminalProjection;
2040
- return SOLE_CALL_REJECTION[call.name] !== undefined
2041
- || terminalProjection?.requiresSoleCall === true;
2042
- });
2043
- if (callbacks.settleOutputMode === "final_only" && containsSoleCallOnlyViolation) {
2044
- streamCallbackBuffer?.discard();
2045
- }
2046
- else {
2047
- await streamCallbackBuffer?.flush();
2048
- }
2049
- pushGenerated(msg);
2050
- // Execute tools (sole-call tools in mixed calls are rejected inline)
2051
- for (const tc of result.toolCalls) {
2052
- if (signal?.aborted)
2053
- break;
2054
- const requiredDispatchRejection = requiredDispatchRejections.get(tc.id);
2055
- if (requiredDispatchRejection) {
2056
- callbacks.onToolStart(tc.name, requiredDispatchRejection.args);
2057
- callbacks.onToolEnd(tc.name, (0, tools_1.summarizeArgs)(tc.name, requiredDispatchRejection.args), false);
2058
- pushGenerated({ role: "tool", tool_call_id: tc.id, content: requiredDispatchRejection.message });
2059
- providerRuntime.appendToolOutput(tc.id, requiredDispatchRejection.message);
2060
- options?.toolBoundaryObserver?.({
2061
- name: tc.name,
2062
- reason: "dependency_rejected",
2063
- globallyResolvable: typeof (0, tools_1.resolveToolDefinition)(tc.name)?.handler === "function",
2064
- invoked: false,
2065
- sideEffect: false,
1993
+ if (protectedCall && protectedCall.policy.kind === "required") {
1994
+ if (!options?.approvalCoordinator) {
1995
+ streamCallbackBuffer?.discard();
1996
+ pushGenerated(msg);
1997
+ const rejection = "rejected: this protected tool requires approval, but the approval coordinator is unavailable; the handler was not invoked.";
1998
+ pushGenerated({ role: "tool", tool_call_id: protectedCall.call.id, content: rejection });
1999
+ providerRuntime.appendToolOutput(protectedCall.call.id, rejection);
2000
+ (0, runtime_1.emitNervesEvent)({
2001
+ level: "warn",
2002
+ component: "engine",
2003
+ event: "engine.approval_coordinator_unavailable",
2004
+ message: "protected tool failed closed before handler execution",
2005
+ meta: { toolName: protectedCall.call.name, toolCallId: protectedCall.call.id },
2006
+ });
2007
+ continue;
2008
+ }
2009
+ streamCallbackBuffer?.discard();
2010
+ pushGenerated(msg);
2011
+ const toolDigest = (0, tool_arguments_1.digestJson)({
2012
+ name: protectedCall.call.name,
2013
+ schemaDigest: protectedCall.validated.schemaDigest,
2014
+ policyId: protectedCall.policy.policyId,
2015
+ });
2016
+ const policyDigest = (0, tool_arguments_1.digestJson)({
2017
+ policyId: protectedCall.policy.policyId,
2018
+ actionClass: protectedCall.policy.actionClass,
2019
+ classification: "required",
2066
2020
  });
2021
+ const committed = await options.approvalCoordinator.propose({
2022
+ toolCall: structuredClone(msg.tool_calls[0]),
2023
+ arguments: structuredClone(protectedCall.validated.arguments),
2024
+ preCallMessages,
2025
+ frozenAssistantMessage: structuredClone(msg),
2026
+ schemaDigest: protectedCall.validated.schemaDigest,
2027
+ toolDigest,
2028
+ policyDigest,
2029
+ policyId: protectedCall.policy.policyId,
2030
+ actionClass: protectedCall.policy.actionClass,
2031
+ });
2032
+ suspension = {
2033
+ approvalId: committed.approvalId,
2034
+ toolCallId: protectedCall.call.id,
2035
+ checkpointDigest: committed.checkpointDigest,
2036
+ suspendedSessionRevision: committed.suspendedSessionRevision,
2037
+ };
2038
+ outcome = "suspended";
2039
+ done = true;
2067
2040
  (0, runtime_1.emitNervesEvent)({
2068
- level: "warn",
2069
2041
  component: "engine",
2070
- event: "engine.required_tool_dispatch_rejected",
2071
- message: "required tool dependency rejected before approval and handler dispatch",
2072
- meta: { toolName: tc.name },
2042
+ event: "engine.approval_turn_suspended",
2043
+ message: "agent turn suspended before protected tool execution",
2044
+ meta: { approvalId: committed.approvalId, toolCallId: protectedCall.call.id },
2073
2045
  });
2074
2046
  continue;
2075
2047
  }
2076
- // Reject sole-call tools when mixed with other tool calls
2077
- const terminalProjection = (0, tools_1.resolveToolDefinition)(tc.name)?.terminalProjection;
2078
- const soleCallRejection = SOLE_CALL_REJECTION[tc.name]
2079
- ?? (terminalProjection?.requiresSoleCall
2080
- ? `rejected: ${tc.name} must be the only tool call.`
2081
- : undefined);
2082
- if (soleCallRejection) {
2083
- pushGenerated({ role: "tool", tool_call_id: tc.id, content: soleCallRejection });
2084
- providerRuntime.appendToolOutput(tc.id, soleCallRejection);
2085
- continue;
2086
- }
2087
- const args = validatedCallArguments.get(tc);
2088
- const currentEffectFingerprint = effectFingerprint(tc.name, tc.arguments);
2089
- if (forcingHistoricalEffect
2090
- && currentEffectFingerprint
2091
- && !unresolvedHistoricalEffects.some((effect) => effect.fingerprint === currentEffectFingerprint)) {
2092
- const rejection = "rejected: this turn is retrying an unresolved historical effect, and these mutation arguments do not match it. Read current state or retry the exact failed effect.";
2093
- callbacks.onToolStart(tc.name, args);
2094
- callbacks.onToolEnd(tc.name, (0, tools_1.summarizeArgs)(tc.name, args), false);
2095
- pushGenerated({ role: "tool", tool_call_id: tc.id, content: rejection });
2096
- providerRuntime.appendToolOutput(tc.id, rejection);
2097
- continue;
2048
+ const containsSoleCallOnlyViolation = result.toolCalls.length > 1
2049
+ && result.toolCalls.some((call) => {
2050
+ const terminalProjection = (0, tools_1.resolveToolDefinition)(call.name)?.terminalProjection;
2051
+ return SOLE_CALL_REJECTION[call.name] !== undefined
2052
+ || terminalProjection?.requiresSoleCall === true;
2053
+ });
2054
+ if (callbacks.settleOutputMode === "final_only" && containsSoleCallOnlyViolation) {
2055
+ streamCallbackBuffer?.discard();
2098
2056
  }
2099
- if (tc.name === "send_message" && args.friendId === "self") {
2100
- const latestUserText = latestUserMessageText(messages);
2101
- if (!isPrivateRuntimeChannel && looksLikePrivateReturnRequest(latestUserText)) {
2102
- const argSummary = (0, tools_1.summarizeArgs)(tc.name, args);
2103
- const rejection = "private-return requests must use ponder, not send_message(friendId=self). Create a typed ponder packet with the marker/source request preserved, then only acknowledge that the private pass is queued.";
2104
- callbacks.onToolStart(tc.name, args);
2105
- callbacks.onToolEnd(tc.name, argSummary, false);
2106
- pushGenerated({ role: "tool", tool_call_id: tc.id, content: rejection });
2107
- providerRuntime.appendToolOutput(tc.id, rejection);
2108
- continue;
2109
- }
2110
- sawSendMessageSelf = true;
2057
+ else {
2058
+ await streamCallbackBuffer?.flush();
2111
2059
  }
2112
- if (tc.name === "speak") {
2113
- // The canonical pre-batch schema gate guarantees a required string.
2114
- const speakArgs = JSON.parse(tc.arguments);
2115
- const speakMessage = speakArgs.message;
2116
- const argSummary = (0, tools_1.summarizeArgs)("speak", { message: speakMessage });
2117
- callbacks.onToolStart("speak", { message: speakMessage });
2118
- if (speakMessage.trim().length === 0) {
2119
- const err = "speak requires a non-empty `message` string.";
2120
- callbacks.onToolEnd("speak", argSummary, false);
2121
- pushGenerated({ role: "tool", tool_call_id: tc.id, content: err });
2122
- providerRuntime.appendToolOutput(tc.id, err);
2060
+ pushGenerated(msg);
2061
+ // Execute tools (sole-call tools in mixed calls are rejected inline)
2062
+ for (const tc of result.toolCalls) {
2063
+ if (signal?.aborted)
2064
+ break;
2065
+ const requiredDispatchRejection = requiredDispatchRejections.get(tc.id);
2066
+ if (requiredDispatchRejection) {
2067
+ callbacks.onToolStart(tc.name, requiredDispatchRejection.args);
2068
+ callbacks.onToolEnd(tc.name, (0, tools_1.summarizeArgs)(tc.name, requiredDispatchRejection.args), false);
2069
+ pushGenerated({ role: "tool", tool_call_id: tc.id, content: requiredDispatchRejection.message });
2070
+ providerRuntime.appendToolOutput(tc.id, requiredDispatchRejection.message);
2071
+ options?.toolBoundaryObserver?.({
2072
+ name: tc.name,
2073
+ reason: "dependency_rejected",
2074
+ globallyResolvable: typeof (0, tools_1.resolveToolDefinition)(tc.name)?.handler === "function",
2075
+ invoked: false,
2076
+ sideEffect: false,
2077
+ });
2123
2078
  (0, runtime_1.emitNervesEvent)({
2124
2079
  level: "warn",
2125
2080
  component: "engine",
2126
- event: "engine.speak_invalid",
2127
- message: "speak rejected: missing or empty message",
2128
- meta: {},
2081
+ event: "engine.required_tool_dispatch_rejected",
2082
+ message: "required tool dependency rejected before approval and handler dispatch",
2083
+ meta: { toolName: tc.name },
2129
2084
  });
2130
2085
  continue;
2131
2086
  }
2132
- callbacks.onTextChunk(speakMessage);
2133
- let speakDeliveryError = null;
2134
- try {
2135
- await callbacks.flushNow?.();
2087
+ // Reject sole-call tools when mixed with other tool calls
2088
+ const terminalProjection = (0, tools_1.resolveToolDefinition)(tc.name)?.terminalProjection;
2089
+ const soleCallRejection = SOLE_CALL_REJECTION[tc.name]
2090
+ ?? (terminalProjection?.requiresSoleCall
2091
+ ? `rejected: ${tc.name} must be the only tool call.`
2092
+ : undefined);
2093
+ if (soleCallRejection) {
2094
+ pushGenerated({ role: "tool", tool_call_id: tc.id, content: soleCallRejection });
2095
+ providerRuntime.appendToolOutput(tc.id, soleCallRejection);
2096
+ continue;
2136
2097
  }
2137
- catch (err) {
2138
- speakDeliveryError = err instanceof Error ? err : new Error(String(err));
2098
+ const args = validatedCallArguments.get(tc);
2099
+ const currentEffectFingerprint = effectFingerprint(tc.name, tc.arguments);
2100
+ if (forcingHistoricalEffect
2101
+ && currentEffectFingerprint
2102
+ && !unresolvedHistoricalEffects.some((effect) => effect.fingerprint === currentEffectFingerprint)) {
2103
+ const rejection = "rejected: this turn is retrying an unresolved historical effect, and these mutation arguments do not match it. Read current state or retry the exact failed effect.";
2104
+ callbacks.onToolStart(tc.name, args);
2105
+ callbacks.onToolEnd(tc.name, (0, tools_1.summarizeArgs)(tc.name, args), false);
2106
+ pushGenerated({ role: "tool", tool_call_id: tc.id, content: rejection });
2107
+ providerRuntime.appendToolOutput(tc.id, rejection);
2108
+ continue;
2139
2109
  }
2140
- if (speakDeliveryError) {
2141
- callbacks.onToolEnd("speak", argSummary, false);
2142
- const failMsg = `speak delivery failed: ${speakDeliveryError.message}. the message did not reach your friend; do not assume they saw it.`;
2143
- pushGenerated({ role: "tool", tool_call_id: tc.id, content: failMsg });
2144
- providerRuntime.appendToolOutput(tc.id, failMsg);
2110
+ if (tc.name === "send_message" && args.friendId === "self") {
2111
+ const latestUserText = latestUserMessageText(messages);
2112
+ if (!isPrivateRuntimeChannel && looksLikePrivateReturnRequest(latestUserText)) {
2113
+ const argSummary = (0, tools_1.summarizeArgs)(tc.name, args);
2114
+ const rejection = "private-return requests must use ponder, not send_message(friendId=self). Create a typed ponder packet with the marker/source request preserved, then only acknowledge that the private pass is queued.";
2115
+ callbacks.onToolStart(tc.name, args);
2116
+ callbacks.onToolEnd(tc.name, argSummary, false);
2117
+ pushGenerated({ role: "tool", tool_call_id: tc.id, content: rejection });
2118
+ providerRuntime.appendToolOutput(tc.id, rejection);
2119
+ continue;
2120
+ }
2121
+ sawSendMessageSelf = true;
2122
+ }
2123
+ if (tc.name === "speak") {
2124
+ // The canonical pre-batch schema gate guarantees a required string.
2125
+ const speakArgs = JSON.parse(tc.arguments);
2126
+ const speakMessage = speakArgs.message;
2127
+ const argSummary = (0, tools_1.summarizeArgs)("speak", { message: speakMessage });
2128
+ callbacks.onToolStart("speak", { message: speakMessage });
2129
+ if (speakMessage.trim().length === 0) {
2130
+ const err = "speak requires a non-empty `message` string.";
2131
+ callbacks.onToolEnd("speak", argSummary, false);
2132
+ pushGenerated({ role: "tool", tool_call_id: tc.id, content: err });
2133
+ providerRuntime.appendToolOutput(tc.id, err);
2134
+ (0, runtime_1.emitNervesEvent)({
2135
+ level: "warn",
2136
+ component: "engine",
2137
+ event: "engine.speak_invalid",
2138
+ message: "speak rejected: missing or empty message",
2139
+ meta: {},
2140
+ });
2141
+ continue;
2142
+ }
2143
+ callbacks.onTextChunk(speakMessage);
2144
+ let speakDeliveryError = null;
2145
+ try {
2146
+ await callbacks.flushNow?.();
2147
+ }
2148
+ catch (err) {
2149
+ speakDeliveryError = err instanceof Error ? err : new Error(String(err));
2150
+ }
2151
+ if (speakDeliveryError) {
2152
+ callbacks.onToolEnd("speak", argSummary, false);
2153
+ const failMsg = `speak delivery failed: ${speakDeliveryError.message}. the message did not reach your friend; do not assume they saw it.`;
2154
+ pushGenerated({ role: "tool", tool_call_id: tc.id, content: failMsg });
2155
+ providerRuntime.appendToolOutput(tc.id, failMsg);
2156
+ (0, runtime_1.emitNervesEvent)({
2157
+ level: "error",
2158
+ component: "engine",
2159
+ event: "engine.speak_delivery_failed",
2160
+ message: "speak delivery failed",
2161
+ meta: { error: speakDeliveryError.message, messageLength: speakMessage.length },
2162
+ });
2163
+ continue;
2164
+ }
2165
+ callbacks.onToolEnd("speak", argSummary, true);
2166
+ const ack = "(spoken)";
2167
+ pushGenerated({ role: "tool", tool_call_id: tc.id, content: ack });
2168
+ providerRuntime.appendToolOutput(tc.id, ack);
2145
2169
  (0, runtime_1.emitNervesEvent)({
2146
- level: "error",
2147
2170
  component: "engine",
2148
- event: "engine.speak_delivery_failed",
2149
- message: "speak delivery failed",
2150
- meta: { error: speakDeliveryError.message, messageLength: speakMessage.length },
2171
+ event: "engine.speak",
2172
+ message: "agent spoke mid-turn",
2173
+ meta: { messageLength: speakMessage.length },
2151
2174
  });
2152
2175
  continue;
2153
2176
  }
2154
- callbacks.onToolEnd("speak", argSummary, true);
2155
- const ack = "(spoken)";
2156
- pushGenerated({ role: "tool", tool_call_id: tc.id, content: ack });
2157
- providerRuntime.appendToolOutput(tc.id, ack);
2158
- (0, runtime_1.emitNervesEvent)({
2159
- component: "engine",
2160
- event: "engine.speak",
2161
- message: "agent spoke mid-turn",
2162
- meta: { messageLength: speakMessage.length },
2163
- });
2164
- continue;
2165
- }
2166
- if (tc.name === "ponder") {
2167
- const parsedArgs = normalizeLegacyPonderArgs(parsePonderPayload(tc.arguments));
2168
- const argSummary = (0, tools_1.summarizeArgs)(tc.name, parsedArgs);
2169
- callbacks.onToolStart(tc.name, parsedArgs);
2170
- let toolResult;
2171
- let success = false;
2172
- try {
2173
- const action = parsedArgs.action ?? "create";
2174
- const currentSession = augmentedToolContext?.currentSession;
2175
- const currentOrigin = currentSession
2176
- ? { friendId: currentSession.friendId, channel: currentSession.channel, key: currentSession.key }
2177
- : undefined;
2178
- const isInnerChannel = currentOrigin?.friendId === "self" && currentOrigin?.channel === "inner";
2179
- const shouldCreateReturnObligation = !!currentOrigin && !isInnerChannel;
2180
- const attentionQueue = augmentedToolContext?.delegatedOrigins ?? [];
2181
- const successCriteria = parseSuccessCriteria(parsedArgs.success_criteria);
2182
- const payload = parsePacketPayload(parsedArgs.payload_json);
2183
- let packet;
2184
- let returnObligationId = null;
2185
- let resultAction = "created";
2186
- let privateReturnSourceRequest = "";
2187
- if (action === "create") {
2188
- if (isInnerChannel && attentionQueue.length > 0) {
2189
- throw new Error("private runtime already has held return work in the attention queue; surface the existing delegationId instead of creating a replacement ponder packet.");
2190
- }
2191
- const kind = parsedArgs.kind;
2192
- const objective = typeof parsedArgs.objective === "string" ? parsedArgs.objective.trim() : "";
2193
- const summary = typeof parsedArgs.summary === "string" ? parsedArgs.summary.trim() : "";
2194
- const sourceRequest = currentOrigin && !isInnerChannel ? latestUserMessageText(messages) : "";
2195
- privateReturnSourceRequest = sourceRequest;
2196
- if (!kind || !objective || !successCriteria || !payload) {
2197
- throw new Error("ponder create requires kind, objective, success_criteria, and valid payload_json.");
2198
- }
2199
- const packetPayload = sourceRequest
2200
- ? { ...payload, sourceRequest }
2201
- : payload;
2202
- const createLinkedReturnObligation = (id, packetId) => {
2203
- (0, obligations_1.createReturnObligation)((0, identity_2.getAgentName)(), {
2204
- id,
2205
- origin: currentOrigin,
2206
- status: "queued",
2207
- delegatedContent: buildPonderDelegatedContent({ summary, objective, sourceRequest }),
2208
- packetId,
2209
- createdAt: Date.now(),
2210
- });
2211
- };
2212
- const agentRoot = (0, identity_2.getAgentRoot)();
2213
- let relatedObligationId;
2214
- if (currentOrigin && !isInnerChannel) {
2215
- try {
2216
- const obligation = (0, obligations_1.createObligation)(agentRoot, {
2177
+ if (tc.name === "ponder") {
2178
+ const parsedArgs = normalizeLegacyPonderArgs(parsePonderPayload(tc.arguments));
2179
+ const argSummary = (0, tools_1.summarizeArgs)(tc.name, parsedArgs);
2180
+ callbacks.onToolStart(tc.name, parsedArgs);
2181
+ let toolResult;
2182
+ let success = false;
2183
+ try {
2184
+ const action = parsedArgs.action ?? "create";
2185
+ const currentSession = augmentedToolContext?.currentSession;
2186
+ const currentOrigin = currentSession
2187
+ ? { friendId: currentSession.friendId, channel: currentSession.channel, key: currentSession.key }
2188
+ : undefined;
2189
+ const isInnerChannel = currentOrigin?.friendId === "self" && currentOrigin?.channel === "inner";
2190
+ const shouldCreateReturnObligation = !!currentOrigin && !isInnerChannel;
2191
+ const attentionQueue = augmentedToolContext?.delegatedOrigins ?? [];
2192
+ const successCriteria = parseSuccessCriteria(parsedArgs.success_criteria);
2193
+ const payload = parsePacketPayload(parsedArgs.payload_json);
2194
+ let packet;
2195
+ let returnObligationId = null;
2196
+ let resultAction = "created";
2197
+ let privateReturnSourceRequest = "";
2198
+ if (action === "create") {
2199
+ if (isInnerChannel && attentionQueue.length > 0) {
2200
+ throw new Error("private runtime already has held return work in the attention queue; surface the existing delegationId instead of creating a replacement ponder packet.");
2201
+ }
2202
+ const kind = parsedArgs.kind;
2203
+ const objective = typeof parsedArgs.objective === "string" ? parsedArgs.objective.trim() : "";
2204
+ const summary = typeof parsedArgs.summary === "string" ? parsedArgs.summary.trim() : "";
2205
+ const sourceRequest = currentOrigin && !isInnerChannel ? latestUserMessageText(messages) : "";
2206
+ privateReturnSourceRequest = sourceRequest;
2207
+ if (!kind || !objective || !successCriteria || !payload) {
2208
+ throw new Error("ponder create requires kind, objective, success_criteria, and valid payload_json.");
2209
+ }
2210
+ const packetPayload = sourceRequest
2211
+ ? { ...payload, sourceRequest }
2212
+ : payload;
2213
+ const createLinkedReturnObligation = (id, packetId) => {
2214
+ (0, obligations_1.createReturnObligation)((0, identity_2.getAgentName)(), {
2215
+ id,
2217
2216
  origin: currentOrigin,
2218
- content: objective,
2217
+ status: "queued",
2218
+ delegatedContent: buildPonderDelegatedContent({ summary, objective, sourceRequest }),
2219
+ packetId,
2220
+ createdAt: Date.now(),
2219
2221
  });
2220
- relatedObligationId = obligation.id;
2222
+ };
2223
+ const agentRoot = (0, identity_2.getAgentRoot)();
2224
+ let relatedObligationId;
2225
+ if (currentOrigin && !isInnerChannel) {
2226
+ try {
2227
+ const obligation = (0, obligations_1.createObligation)(agentRoot, {
2228
+ origin: currentOrigin,
2229
+ content: objective,
2230
+ });
2231
+ relatedObligationId = obligation.id;
2232
+ }
2233
+ catch {
2234
+ relatedObligationId = undefined;
2235
+ }
2221
2236
  }
2222
- catch {
2223
- relatedObligationId = undefined;
2224
- }
2225
- }
2226
- const frictionSignature = kind === "harness_friction" && typeof packetPayload.frictionSignature === "string"
2227
- ? packetPayload.frictionSignature
2228
- : null;
2229
- const existing = frictionSignature && currentOrigin
2230
- ? (0, packets_1.findHarnessFrictionPacket)(agentRoot, currentOrigin, frictionSignature)
2231
- : null;
2232
- if (existing) {
2233
- resultAction = "revised";
2234
- const existingActiveReturnId = shouldCreateReturnObligation
2235
- ? activeReturnObligationId((0, identity_2.getAgentName)(), existing.relatedReturnObligationId)
2237
+ const frictionSignature = kind === "harness_friction" && typeof packetPayload.frictionSignature === "string"
2238
+ ? packetPayload.frictionSignature
2239
+ : null;
2240
+ const existing = frictionSignature && currentOrigin
2241
+ ? (0, packets_1.findHarnessFrictionPacket)(agentRoot, currentOrigin, frictionSignature)
2236
2242
  : null;
2237
- returnObligationId = existingActiveReturnId
2238
- ?? (shouldCreateReturnObligation ? (0, obligations_1.generateObligationId)(Date.now()) : null);
2239
- packet = existing.status === "drafting"
2240
- ? (0, packets_1.revisePonderPacket)(agentRoot, existing.id, {
2243
+ if (existing) {
2244
+ resultAction = "revised";
2245
+ const existingActiveReturnId = shouldCreateReturnObligation
2246
+ ? activeReturnObligationId((0, identity_2.getAgentName)(), existing.relatedReturnObligationId)
2247
+ : null;
2248
+ returnObligationId = existingActiveReturnId
2249
+ ?? (shouldCreateReturnObligation ? (0, obligations_1.generateObligationId)(Date.now()) : null);
2250
+ packet = existing.status === "drafting"
2251
+ ? (0, packets_1.revisePonderPacket)(agentRoot, existing.id, {
2252
+ kind,
2253
+ objective,
2254
+ summary,
2255
+ successCriteria,
2256
+ payload: packetPayload,
2257
+ })
2258
+ : existing;
2259
+ if (returnObligationId && returnObligationId !== existing.relatedReturnObligationId) {
2260
+ packet = (0, packets_1.advancePonderPacket)(agentRoot, packet.id, { relatedReturnObligationId: returnObligationId });
2261
+ createLinkedReturnObligation(returnObligationId, packet.id);
2262
+ }
2263
+ }
2264
+ else {
2265
+ returnObligationId = shouldCreateReturnObligation ? (0, obligations_1.generateObligationId)(Date.now()) : null;
2266
+ packet = (0, packets_1.createPonderPacket)(agentRoot, {
2241
2267
  kind,
2242
2268
  objective,
2243
2269
  summary,
2244
2270
  successCriteria,
2271
+ ...(currentOrigin ? { origin: currentOrigin } : {}),
2272
+ ...(relatedObligationId ? { relatedObligationId } : {}),
2273
+ ...(returnObligationId ? { relatedReturnObligationId: returnObligationId } : {}),
2274
+ ...(parsedArgs.follows_packet_id ? { followsPacketId: parsedArgs.follows_packet_id } : {}),
2245
2275
  payload: packetPayload,
2246
- })
2247
- : existing;
2248
- if (returnObligationId && returnObligationId !== existing.relatedReturnObligationId) {
2249
- packet = (0, packets_1.advancePonderPacket)(agentRoot, packet.id, { relatedReturnObligationId: returnObligationId });
2250
- createLinkedReturnObligation(returnObligationId, packet.id);
2276
+ });
2277
+ if (returnObligationId) {
2278
+ createLinkedReturnObligation(returnObligationId, packet.id);
2279
+ }
2251
2280
  }
2252
2281
  }
2253
2282
  else {
2254
- returnObligationId = shouldCreateReturnObligation ? (0, obligations_1.generateObligationId)(Date.now()) : null;
2255
- packet = (0, packets_1.createPonderPacket)(agentRoot, {
2283
+ const packetId = typeof parsedArgs.packet_id === "string" ? parsedArgs.packet_id.trim() : "";
2284
+ const kind = parsedArgs.kind;
2285
+ const objective = typeof parsedArgs.objective === "string" ? parsedArgs.objective.trim() : "";
2286
+ const summary = typeof parsedArgs.summary === "string" ? parsedArgs.summary.trim() : "";
2287
+ if (!packetId || !kind || !objective || !successCriteria || !payload) {
2288
+ throw new Error("ponder revise requires packet_id, kind, objective, success_criteria, and valid payload_json.");
2289
+ }
2290
+ packet = (0, packets_1.revisePonderPacket)((0, identity_2.getAgentRoot)(), packetId, {
2256
2291
  kind,
2257
2292
  objective,
2258
2293
  summary,
2259
2294
  successCriteria,
2260
- ...(currentOrigin ? { origin: currentOrigin } : {}),
2261
- ...(relatedObligationId ? { relatedObligationId } : {}),
2262
- ...(returnObligationId ? { relatedReturnObligationId: returnObligationId } : {}),
2263
- ...(parsedArgs.follows_packet_id ? { followsPacketId: parsedArgs.follows_packet_id } : {}),
2264
- payload: packetPayload,
2295
+ payload,
2265
2296
  });
2266
- if (returnObligationId) {
2267
- createLinkedReturnObligation(returnObligationId, packet.id);
2268
- }
2297
+ returnObligationId = packet.relatedReturnObligationId
2298
+ && !(packet.origin?.friendId === "self" && packet.origin.channel === "inner")
2299
+ ? packet.relatedReturnObligationId
2300
+ : null;
2301
+ resultAction = "revised";
2269
2302
  }
2270
- }
2271
- else {
2272
- const packetId = typeof parsedArgs.packet_id === "string" ? parsedArgs.packet_id.trim() : "";
2273
- const kind = parsedArgs.kind;
2274
- const objective = typeof parsedArgs.objective === "string" ? parsedArgs.objective.trim() : "";
2275
- const summary = typeof parsedArgs.summary === "string" ? parsedArgs.summary.trim() : "";
2276
- if (!packetId || !kind || !objective || !successCriteria || !payload) {
2277
- throw new Error("ponder revise requires packet_id, kind, objective, success_criteria, and valid payload_json.");
2303
+ if (returnObligationId) {
2304
+ for (const token of extractPrivateReturnHeldTokens(privateReturnSourceRequest)) {
2305
+ privateReturnHeldTokens.add(token);
2306
+ }
2307
+ const agentName = (0, identity_2.getAgentName)();
2308
+ await (0, socket_client_1.requestPrivateWake)(agentName, augmentedToolContext?.daemonSocketPath, buildPonderReturnPrivateWakeOptions({ agentName, packet, returnObligationId })).catch(() => undefined);
2278
2309
  }
2279
- packet = (0, packets_1.revisePonderPacket)((0, identity_2.getAgentRoot)(), packetId, {
2280
- kind,
2281
- objective,
2282
- summary,
2283
- successCriteria,
2284
- payload,
2310
+ sawPonder = true;
2311
+ toolResult = buildPonderResult(packet, resultAction, returnObligationId);
2312
+ success = true;
2313
+ (0, runtime_1.emitNervesEvent)({
2314
+ component: "engine",
2315
+ event: "engine.ponder_packet",
2316
+ message: "ponder packet touched",
2317
+ meta: {
2318
+ action: resultAction,
2319
+ packetId: packet.id,
2320
+ kind: packet.kind,
2321
+ status: packet.status,
2322
+ },
2285
2323
  });
2286
- returnObligationId = packet.relatedReturnObligationId
2287
- && !(packet.origin?.friendId === "self" && packet.origin.channel === "inner")
2288
- ? packet.relatedReturnObligationId
2289
- : null;
2290
- resultAction = "revised";
2291
2324
  }
2292
- if (returnObligationId) {
2293
- for (const token of extractPrivateReturnHeldTokens(privateReturnSourceRequest)) {
2294
- privateReturnHeldTokens.add(token);
2295
- }
2296
- const agentName = (0, identity_2.getAgentName)();
2297
- await (0, socket_client_1.requestPrivateWake)(agentName, augmentedToolContext?.daemonSocketPath, buildPonderReturnPrivateWakeOptions({ agentName, packet, returnObligationId })).catch(() => undefined);
2325
+ catch (error) {
2326
+ toolResult = error instanceof Error ? error.message : String(error);
2298
2327
  }
2299
- sawPonder = true;
2300
- toolResult = buildPonderResult(packet, resultAction, returnObligationId);
2301
- success = true;
2302
- (0, runtime_1.emitNervesEvent)({
2303
- component: "engine",
2304
- event: "engine.ponder_packet",
2305
- message: "ponder packet touched",
2306
- meta: {
2307
- action: resultAction,
2308
- packetId: packet.id,
2309
- kind: packet.kind,
2310
- status: packet.status,
2311
- },
2312
- });
2328
+ callbacks.onToolEnd(tc.name, argSummary, success);
2329
+ pushGenerated({ role: "tool", tool_call_id: tc.id, content: toolResult });
2330
+ providerRuntime.appendToolOutput(tc.id, toolResult);
2331
+ continue;
2313
2332
  }
2314
- catch (error) {
2315
- toolResult = error instanceof Error ? error.message : String(error);
2333
+ /* v8 ignore next -- flag tested via truth-check integration tests @preserve */
2334
+ if (tc.name === "query_session")
2335
+ sawQuerySession = true;
2336
+ /* v8 ignore next -- flag tested via truth-check integration tests @preserve */
2337
+ if (tc.name === "bridge_manage")
2338
+ sawBridgeManage = true;
2339
+ /* v8 ignore next -- flag tested via truth-check integration tests @preserve */
2340
+ if (isExternalStateQuery(tc.name, args))
2341
+ sawExternalStateQuery = true;
2342
+ const argSummary = (0, tools_1.summarizeArgs)(tc.name, args);
2343
+ const toolLoop = (0, tool_loop_1.detectToolLoop)(toolLoopState, tc.name, args);
2344
+ if (toolLoop.stuck) {
2345
+ const rejection = `loop guard: ${toolLoop.message}`;
2346
+ callbacks.onToolStart(tc.name, args);
2347
+ callbacks.onToolEnd(tc.name, argSummary, false);
2348
+ pushGenerated({ role: "tool", tool_call_id: tc.id, content: rejection });
2349
+ providerRuntime.appendToolOutput(tc.id, rejection);
2350
+ continue;
2316
2351
  }
2317
- callbacks.onToolEnd(tc.name, argSummary, success);
2318
- pushGenerated({ role: "tool", tool_call_id: tc.id, content: toolResult });
2319
- providerRuntime.appendToolOutput(tc.id, toolResult);
2320
- continue;
2321
- }
2322
- /* v8 ignore next -- flag tested via truth-check integration tests @preserve */
2323
- if (tc.name === "query_session")
2324
- sawQuerySession = true;
2325
- /* v8 ignore next -- flag tested via truth-check integration tests @preserve */
2326
- if (tc.name === "bridge_manage")
2327
- sawBridgeManage = true;
2328
- /* v8 ignore next -- flag tested via truth-check integration tests @preserve */
2329
- if (isExternalStateQuery(tc.name, args))
2330
- sawExternalStateQuery = true;
2331
- const argSummary = (0, tools_1.summarizeArgs)(tc.name, args);
2332
- const toolLoop = (0, tool_loop_1.detectToolLoop)(toolLoopState, tc.name, args);
2333
- if (toolLoop.stuck) {
2334
- const rejection = `loop guard: ${toolLoop.message}`;
2335
2352
  callbacks.onToolStart(tc.name, args);
2336
- callbacks.onToolEnd(tc.name, argSummary, false);
2337
- pushGenerated({ role: "tool", tool_call_id: tc.id, content: rejection });
2338
- providerRuntime.appendToolOutput(tc.id, rejection);
2339
- continue;
2340
- }
2341
- callbacks.onToolStart(tc.name, args);
2342
- let toolResult;
2343
- let success;
2344
- try {
2345
- const execToolFn = options?.execTool ?? tools_1.execTool;
2346
- const routineActionSelection = approvalCalls.find((entry) => entry.call.id === tc.id)?.routineActionSelection;
2347
- const executionToolContext = routineActionSelection && augmentedToolContext ? { ...augmentedToolContext, routineActionSelection } : augmentedToolContext;
2348
- if (requiredToolCallNames.includes(tc.name) && !options?.requiredToolCalls?.requireSuccessfulResults)
2353
+ let toolResult;
2354
+ let success;
2355
+ try {
2356
+ const execToolFn = options?.execTool ?? tools_1.execTool;
2357
+ const routineActionSelection = approvalCalls.find((entry) => entry.call.id === tc.id)?.routineActionSelection;
2358
+ const executionToolContext = routineActionSelection && augmentedToolContext ? { ...augmentedToolContext, routineActionSelection } : augmentedToolContext;
2359
+ if (requiredToolCallNames.includes(tc.name) && !options?.requiredToolCalls?.requireSuccessfulResults)
2360
+ dispatchedRequiredToolCalls.add(tc.name);
2361
+ toolResult = await execToolFn(tc.name, args, executionToolContext);
2362
+ success = true;
2363
+ }
2364
+ catch (e) {
2365
+ toolResult = `error: ${e}`;
2366
+ success = false;
2367
+ augmentedToolContext?.habitSession?.recordError?.(toolResult);
2368
+ }
2369
+ const validatedRequiredResult = success && requiredToolCallNames.includes(tc.name) && options?.requiredToolCalls?.requireSuccessfulResults
2370
+ ? requiredToolResultSucceeded(tc.name, toolResult, args, options.requiredToolCalls.validateRequiredToolResult)
2371
+ : false;
2372
+ if (validatedRequiredResult) {
2349
2373
  dispatchedRequiredToolCalls.add(tc.name);
2350
- toolResult = await execToolFn(tc.name, args, executionToolContext);
2351
- success = true;
2352
- }
2353
- catch (e) {
2354
- toolResult = `error: ${e}`;
2355
- success = false;
2356
- augmentedToolContext?.habitSession?.recordError?.(toolResult);
2357
- }
2358
- const validatedRequiredResult = success && requiredToolCallNames.includes(tc.name) && options?.requiredToolCalls?.requireSuccessfulResults
2359
- ? requiredToolResultSucceeded(tc.name, toolResult, args, options.requiredToolCalls.validateRequiredToolResult)
2360
- : false;
2361
- if (validatedRequiredResult) {
2362
- dispatchedRequiredToolCalls.add(tc.name);
2363
- for (const requiredName of options?.requiredToolCalls?.requiredToolCallsAfterResult?.(tc.name, args, toolResult) ?? []) {
2364
- if (!candidateToolNames.has(requiredName)) {
2365
- (0, runtime_1.emitNervesEvent)({ level: "error", component: "engine", event: "engine.required_tool_unadvertised", message: "dependent required tool is not advertised for the active channel", meta: { toolName: requiredName, channel: String(channel) } });
2366
- throw new Error(`dependent required tool is not advertised for this channel: ${requiredName}`);
2374
+ for (const requiredName of options?.requiredToolCalls?.requiredToolCallsAfterResult?.(tc.name, args, toolResult) ?? []) {
2375
+ if (!candidateToolNames.has(requiredName)) {
2376
+ (0, runtime_1.emitNervesEvent)({ level: "error", component: "engine", event: "engine.required_tool_unadvertised", message: "dependent required tool is not advertised for the active channel", meta: { toolName: requiredName, channel: String(channel) } });
2377
+ throw new Error(`dependent required tool is not advertised for this channel: ${requiredName}`);
2378
+ }
2379
+ if (!requiredToolCallNames.includes(requiredName))
2380
+ requiredToolCallNames.push(requiredName);
2367
2381
  }
2368
- if (!requiredToolCallNames.includes(requiredName))
2369
- requiredToolCallNames.push(requiredName);
2370
2382
  }
2383
+ if (success && currentEffectFingerprint && !toolResultIndicatesFailure(toolResult)) {
2384
+ unresolvedHistoricalEffects = unresolvedHistoricalEffects.filter((effect) => effect.fingerprint !== currentEffectFingerprint);
2385
+ }
2386
+ const resolvedRiskProfile = (0, tools_1.resolveToolDefinition)(tc.name)?.riskProfile;
2387
+ const toolRiskProfile = typeof resolvedRiskProfile === "function" ? resolvedRiskProfile(args) : resolvedRiskProfile;
2388
+ options?.toolBoundaryObserver?.({
2389
+ name: tc.name,
2390
+ reason: "dispatched",
2391
+ globallyResolvable: typeof (0, tools_1.resolveToolDefinition)(tc.name)?.handler === "function",
2392
+ invoked: true,
2393
+ sideEffect: success && toolRiskProfile?.mutates !== "none",
2394
+ });
2395
+ toolResult = (0, tool_friction_1.rewriteToolResultForModel)(tc.name, toolResult, toolFrictionLedger);
2396
+ (0, tool_loop_1.recordToolOutcome)(toolLoopState, tc.name, args, toolResult, success);
2397
+ callbacks.onToolEnd(tc.name, (0, tools_1.buildToolResultSummary)(tc.name, args, toolResult, success), success);
2398
+ pushGenerated({ role: "tool", tool_call_id: tc.id, content: toolResult });
2399
+ providerRuntime.appendToolOutput(tc.id, toolResult);
2400
+ callbacks.onToolResult?.(messagesWithoutRequiredCorrections());
2371
2401
  }
2372
- if (success && currentEffectFingerprint && !toolResultIndicatesFailure(toolResult)) {
2373
- unresolvedHistoricalEffects = unresolvedHistoricalEffects.filter((effect) => effect.fingerprint !== currentEffectFingerprint);
2374
- }
2375
- const resolvedRiskProfile = (0, tools_1.resolveToolDefinition)(tc.name)?.riskProfile;
2376
- const toolRiskProfile = typeof resolvedRiskProfile === "function" ? resolvedRiskProfile(args) : resolvedRiskProfile;
2377
- options?.toolBoundaryObserver?.({
2378
- name: tc.name,
2379
- reason: "dispatched",
2380
- globallyResolvable: typeof (0, tools_1.resolveToolDefinition)(tc.name)?.handler === "function",
2381
- invoked: true,
2382
- sideEffect: success && toolRiskProfile?.mutates !== "none",
2383
- });
2384
- toolResult = (0, tool_friction_1.rewriteToolResultForModel)(tc.name, toolResult, toolFrictionLedger);
2385
- (0, tool_loop_1.recordToolOutcome)(toolLoopState, tc.name, args, toolResult, success);
2386
- callbacks.onToolEnd(tc.name, (0, tools_1.buildToolResultSummary)(tc.name, args, toolResult, success), success);
2387
- pushGenerated({ role: "tool", tool_call_id: tc.id, content: toolResult });
2388
- providerRuntime.appendToolOutput(tc.id, toolResult);
2389
- callbacks.onToolResult?.(messages);
2390
2402
  }
2391
2403
  }
2392
- }
2393
- catch (e) {
2394
- // Abort is not an error — just stop cleanly
2395
- if (e instanceof provider_attempt_1.ProviderAttemptAbortError || signal?.aborted) {
2396
- stripLastToolCalls(messages);
2397
- stripLastToolCalls(generatedMessages);
2398
- outcome = "aborted";
2399
- break;
2400
- }
2401
- const errorForClassification = e instanceof Error ? e : /* v8 ignore next -- defensive @preserve */ new Error(String(e));
2402
- let providerClassification;
2403
- try {
2404
- providerClassification = providerRuntime.classifyError(errorForClassification);
2405
- }
2406
- catch {
2407
- /* v8 ignore next -- defensive: classifyError should not throw @preserve */
2408
- providerClassification = "unknown";
2404
+ catch (e) {
2405
+ // Abort is not an error — just stop cleanly
2406
+ if (e instanceof provider_attempt_1.ProviderAttemptAbortError || signal?.aborted) {
2407
+ stripLastToolCalls(messages);
2408
+ stripLastToolCalls(generatedMessages);
2409
+ outcome = "aborted";
2410
+ break;
2411
+ }
2412
+ const errorForClassification = e instanceof Error ? e : /* v8 ignore next -- defensive @preserve */ new Error(String(e));
2413
+ let providerClassification;
2414
+ try {
2415
+ providerClassification = providerRuntime.classifyError(errorForClassification);
2416
+ }
2417
+ catch {
2418
+ /* v8 ignore next -- defensive: classifyError should not throw @preserve */
2419
+ providerClassification = "unknown";
2420
+ }
2421
+ finishTerminalProviderError(errorForClassification, providerClassification);
2409
2422
  }
2410
- finishTerminalProviderError(errorForClassification, providerClassification);
2411
2423
  }
2412
2424
  }
2425
+ finally {
2426
+ removeRequiredCorrections();
2427
+ }
2413
2428
  options?.captureGeneratedMessages?.(structuredClone(generatedMessages));
2414
2429
  (0, runtime_1.emitNervesEvent)({
2415
2430
  event: "engine.turn_end",