@ouro.bot/cli 0.1.0-alpha.726 → 0.1.0-alpha.728

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.
Files changed (35) hide show
  1. package/README.md +14 -1
  2. package/assets/bluebubbles-host +264 -0
  3. package/changelog.json +18 -0
  4. package/dist/heart/config.js +1 -2
  5. package/dist/heart/core.js +181 -209
  6. package/dist/heart/daemon/bluebubbles-cli-integration.js +71 -0
  7. package/dist/heart/daemon/bluebubbles-host-protocol.js +433 -0
  8. package/dist/heart/daemon/bluebubbles-host.js +273 -0
  9. package/dist/heart/daemon/cli-defaults.js +8 -1
  10. package/dist/heart/daemon/cli-exec.js +239 -34
  11. package/dist/heart/daemon/cli-help.js +17 -6
  12. package/dist/heart/daemon/cli-parse.js +69 -1
  13. package/dist/heart/daemon/cli-render.js +7 -0
  14. package/dist/heart/daemon/daemon.js +79 -14
  15. package/dist/heart/daemon/doctor.js +21 -1
  16. package/dist/heart/daemon/mcp-canary.js +160 -29
  17. package/dist/heart/daemon/process-manager.js +100 -18
  18. package/dist/heart/daemon/socket-client.js +28 -19
  19. package/dist/heart/providers/anthropic.js +17 -12
  20. package/dist/heart/streaming.js +6 -6
  21. package/dist/heart/versioning/ouro-path-installer.js +30 -24
  22. package/dist/heart/versioning/ouro-recovery-launcher.js +76 -0
  23. package/dist/heart/versioning/version-intent.js +114 -0
  24. package/dist/mind/prompt-budget.js +104 -5
  25. package/dist/repertoire/tools.js +0 -10
  26. package/dist/senses/bluebubbles/client.js +43 -57
  27. package/dist/senses/bluebubbles/context-packet.js +104 -19
  28. package/dist/senses/bluebubbles/entry.js +14 -1
  29. package/dist/senses/bluebubbles/index.js +1398 -719
  30. package/dist/senses/bluebubbles/latest-turn.js +311 -0
  31. package/dist/senses/bluebubbles/outbound-state.js +4 -0
  32. package/dist/senses/bluebubbles/reaction-policy.js +4 -12
  33. package/dist/senses/bluebubbles/semantic-receipts.js +178 -10
  34. package/dist/senses/bluebubbles/webhook-registration.js +228 -0
  35. package/package.json +4 -2
@@ -43,7 +43,10 @@ const provider_credentials_1 = require("./provider-credentials");
43
43
  const habit_session_1 = require("./habits/habit-session");
44
44
  const provider_attempt_1 = require("./provider-attempt");
45
45
  const openai_codex_token_1 = require("./providers/openai-codex-token");
46
- const _providerRuntimes = {
46
+ // Cache only immutable provider construction inputs. ProviderRuntime owns
47
+ // mutable per-turn state (for example Responses nativeInput), so every caller
48
+ // must receive a fresh instance even when its binding fingerprint is unchanged.
49
+ const _providerRuntimeFactories = {
47
50
  human: null,
48
51
  agent: null,
49
52
  };
@@ -108,12 +111,17 @@ function createProviderRegistry() {
108
111
  };
109
112
  }
110
113
  async function getProviderRuntime(facing = "human") {
114
+ let runtime = null;
111
115
  try {
112
116
  const { binding, fingerprint, credential } = await getProviderRuntimeFingerprint(facing);
113
- const cached = _providerRuntimes[facing];
117
+ const cached = _providerRuntimeFactories[facing];
114
118
  if (!cached || cached.fingerprint !== fingerprint) {
115
- const runtime = createProviderRegistry().resolve(binding.provider, binding.model, credential);
116
- _providerRuntimes[facing] = runtime ? { fingerprint, runtime } : null;
119
+ const create = () => createProviderRegistry().resolve(binding.provider, binding.model, credential);
120
+ runtime = create();
121
+ _providerRuntimeFactories[facing] = runtime ? { fingerprint, create } : null;
122
+ }
123
+ else {
124
+ runtime = cached.create();
117
125
  }
118
126
  }
119
127
  catch (error) {
@@ -129,7 +137,7 @@ async function getProviderRuntime(facing = "human") {
129
137
  console.error(`\n[fatal] ${msg}\n`);
130
138
  throw error instanceof Error ? error : new Error(msg);
131
139
  }
132
- if (!_providerRuntimes[facing]) {
140
+ if (!runtime) {
133
141
  const msg = "provider runtime could not be initialized.";
134
142
  (0, runtime_1.emitNervesEvent)({
135
143
  level: "error",
@@ -142,16 +150,16 @@ async function getProviderRuntime(facing = "human") {
142
150
  console.error(`\n[fatal] ${msg}\n`);
143
151
  throw new Error(msg);
144
152
  }
145
- return _providerRuntimes[facing].runtime;
153
+ return runtime;
146
154
  }
147
155
  /**
148
- * Clear the cached provider runtime so the next access re-creates it from
149
- * current config. Runtime access also auto-refreshes when the selected
150
- * provider fingerprint changes on disk.
156
+ * Clear cached provider construction inputs so the next access re-reads them
157
+ * from current config. Runtime instances are always per-caller and are never
158
+ * shared across turns.
151
159
  */
152
160
  function resetProviderRuntime() {
153
- _providerRuntimes.human = null;
154
- _providerRuntimes.agent = null;
161
+ _providerRuntimeFactories.human = null;
162
+ _providerRuntimeFactories.agent = null;
155
163
  }
156
164
  function getModel(facing = "human") {
157
165
  return resolveRuntimeProviderBinding(facing).model;
@@ -348,11 +356,11 @@ async function habitToolBatchBlockReason(habitSession, toolCalls, delegatedOrigi
348
356
  }
349
357
  return null;
350
358
  }
351
- /** Chat-style channels expose the `speak` tool — outer human-conversation channels
352
- * where mid-turn delivery is meaningful. The private runtime has `ponder`. MCP returns
353
- * synchronously. Mail is batch. Anything else (unknown channel) treats as non-chat. */
359
+ /** Channels that deliberately support mid-turn delivery expose `speak`.
360
+ * BlueBubbles is final-only: its adapter owns one accepted visibility boundary.
361
+ * The private runtime has `ponder`; MCP returns synchronously; mail is batch. */
354
362
  function isChatStyleChannel(channel) {
355
- return channel === "cli" || channel === "teams" || channel === "bluebubbles" || channel === "voice";
363
+ return channel === "cli" || channel === "teams" || channel === "voice";
356
364
  }
357
365
  function bindCurrentIngressEvidenceLocator(toolList, evidence) {
358
366
  if (!evidence
@@ -392,35 +400,16 @@ const SOLE_CALL_REJECTION = {
392
400
  rest: "rejected: rest must be the only tool call. finish your work first, then call rest alone.",
393
401
  };
394
402
  function parseSettlePayload(argumentsText) {
395
- try {
396
- const parsed = JSON.parse(argumentsText);
397
- const answer = typeof parsed?.answer === "string" ? parsed.answer : undefined;
398
- const rawIntent = parsed?.intent;
399
- const intent = rawIntent === "complete" || rawIntent === "blocked" || rawIntent === "direct_reply"
400
- ? rawIntent
401
- : undefined;
402
- return { answer, intent };
403
- }
404
- catch {
405
- return {};
406
- }
407
- }
408
- function matchesRegisteredToolArgumentSchema(tool, args) {
409
- const parameters = tool.function.parameters;
410
- for (const requiredKey of parameters.required ?? []) {
411
- if (!(requiredKey in args))
412
- return false;
413
- }
414
- for (const [key, property] of Object.entries(parameters.properties)) {
415
- const value = args[key];
416
- if (value === undefined)
417
- continue;
418
- if (property.type === "string" && typeof value !== "string")
419
- return false;
420
- if (property.enum && !property.enum.includes(value))
421
- return false;
422
- }
423
- return true;
403
+ const parsed = JSON.parse(argumentsText);
404
+ // Provider finalization validates settle.answer as a string before this
405
+ // terminal handling path. Keep this parser focused on projection instead
406
+ // of carrying an unreachable second shape check.
407
+ const answer = parsed.answer;
408
+ const rawIntent = parsed.intent;
409
+ const intent = rawIntent === "complete" || rawIntent === "blocked" || rawIntent === "direct_reply"
410
+ ? rawIntent
411
+ : undefined;
412
+ return { answer, intent };
424
413
  }
425
414
  function parsePonderPayload(argumentsText) {
426
415
  try {
@@ -620,9 +609,10 @@ function getSettleRetryError(mustResolveBeforeHandoff, intent, sawSteeringFollow
620
609
  }
621
610
  return null;
622
611
  }
623
- function upsertSystemPrompt(messages, systemText) {
612
+ function upsertSystemPrompt(messages, systemText, protectedSystemMessages = []) {
624
613
  const systemMessage = { role: "system", content: systemText };
625
- if (messages[0]?.role === "system") {
614
+ const protectedMessages = new Set(protectedSystemMessages.filter((message) => message.role === "system"));
615
+ if (messages[0]?.role === "system" && !protectedMessages.has(messages[0])) {
626
616
  messages[0] = systemMessage;
627
617
  }
628
618
  else {
@@ -808,11 +798,15 @@ function buildAuthFailureGuidance(provider, model, agentName, detail) {
808
798
  return lines.join("\n");
809
799
  }
810
800
  async function runAgent(messages, callbacks, channel, signal, options) {
801
+ const generatedMessages = [];
802
+ const pushGenerated = (...next) => {
803
+ messages.push(...next);
804
+ generatedMessages.push(...structuredClone(next));
805
+ };
811
806
  const facing = (0, friends_1.channelToFacing)(channel);
812
807
  let providerRuntime = await getProviderRuntime(facing);
813
808
  const provider = providerRuntime.id;
814
- const restrictedReactionFeedback = options?.restrictedReactionFeedback === true;
815
- const toolChoiceRequired = restrictedReactionFeedback ? true : options?.toolChoiceRequired ?? true;
809
+ const toolChoiceRequired = options?.toolChoiceRequired ?? true;
816
810
  const traceId = options?.traceId;
817
811
  (0, runtime_1.emitNervesEvent)({
818
812
  event: "engine.turn_start",
@@ -833,6 +827,52 @@ async function runAgent(messages, callbacks, channel, signal, options) {
833
827
  }
834
828
  const turnOrientationFrame = options?.orientationFrame
835
829
  ?? (channel ? (0, orientation_frame_1.buildOrientationFrame)({ channel, messages }) : undefined);
830
+ if (options?.requiredPromptEvidence) {
831
+ let structuralFloor;
832
+ try {
833
+ structuralFloor = (0, prompt_budget_1.assessRequiredPromptEvidenceBudget)({
834
+ messages,
835
+ requiredPromptEvidence: options.requiredPromptEvidence,
836
+ provider: providerRuntime.id,
837
+ model: providerRuntime.model,
838
+ contextWindowTokens: (0, config_1.getContextConfig)().maxTokens,
839
+ });
840
+ }
841
+ catch (error) {
842
+ const invalidEvidenceError = error instanceof Error ? error : new Error(String(error));
843
+ callbacks.onError(invalidEvidenceError, "terminal");
844
+ options.captureGeneratedMessages?.([]);
845
+ (0, runtime_1.emitNervesEvent)({
846
+ event: "engine.turn_end",
847
+ trace_id: traceId,
848
+ component: "engine",
849
+ message: "runAgent turn completed",
850
+ meta: { done: true, sawPonder: false, sawQuerySession: false, sawBridgeManage: false },
851
+ });
852
+ return {
853
+ outcome: "errored",
854
+ error: invalidEvidenceError,
855
+ errorClassification: "unknown",
856
+ };
857
+ }
858
+ if (structuralFloor.status === "required_evidence_over_budget") {
859
+ const budgetError = new Error(`required_evidence_over_budget: required current-turn evidence needs ${structuralFloor.estimatedTokens} tokens but the provider input limit is ${structuralFloor.budget.inputTokenLimit}`);
860
+ callbacks.onError(budgetError, "terminal");
861
+ options.captureGeneratedMessages?.([]);
862
+ (0, runtime_1.emitNervesEvent)({
863
+ event: "engine.turn_end",
864
+ trace_id: traceId,
865
+ component: "engine",
866
+ message: "runAgent turn completed",
867
+ meta: { done: true, sawPonder: false, sawQuerySession: false, sawBridgeManage: false },
868
+ });
869
+ return {
870
+ outcome: "errored",
871
+ error: budgetError,
872
+ errorClassification: "unknown",
873
+ };
874
+ }
875
+ }
836
876
  // Refresh system prompt at start of each turn when channel is provided.
837
877
  // If refresh fails, retain only a recognised stable prefix (or inject a
838
878
  // minimal safe fallback) so stale dynamic state cannot regain authority.
@@ -848,12 +888,22 @@ async function runAgent(messages, callbacks, channel, signal, options) {
848
888
  };
849
889
  const refreshed = await (0, prompt_1.buildSystem)(channel, buildSystemOptions, currentContext);
850
890
  structuredSystemPrompt = refreshed;
851
- upsertSystemPrompt(messages, (0, prompt_1.flattenSystemPrompt)(refreshed));
891
+ upsertSystemPrompt(messages, (0, prompt_1.flattenSystemPrompt)(refreshed), [
892
+ ...(options?.promptOnlyEvidenceMessages ?? []),
893
+ ...(options?.requiredPromptEvidence?.verifiedPredecessorMessage
894
+ ? [options.requiredPromptEvidence.verifiedPredecessorMessage]
895
+ : []),
896
+ ]);
852
897
  }
853
898
  catch (error) {
854
899
  const hadExistingSystemPrompt = messages[0]?.role === "system" && typeof messages[0].content === "string";
855
900
  const fallback = repairFallbackSystemPrompt(turnOrientationFrame);
856
- upsertSystemPrompt(messages, fallback);
901
+ upsertSystemPrompt(messages, fallback, [
902
+ ...(options?.promptOnlyEvidenceMessages ?? []),
903
+ ...(options?.requiredPromptEvidence?.verifiedPredecessorMessage
904
+ ? [options.requiredPromptEvidence.verifiedPredecessorMessage]
905
+ : []),
906
+ ]);
857
907
  (0, runtime_1.emitNervesEvent)({
858
908
  level: "warn",
859
909
  event: "mind.step_error",
@@ -939,25 +989,18 @@ async function runAgent(messages, callbacks, channel, signal, options) {
939
989
  },
940
990
  });
941
991
  stripLastToolCalls(messages);
992
+ stripLastToolCalls(generatedMessages);
942
993
  outcome = "errored";
943
994
  done = true;
944
995
  };
945
- const finishRestrictedReactionFeedbackViolation = (reason) => {
946
- callbacks.onClearText?.();
947
- finishTerminalProviderError(new Error(`restricted reaction feedback failed closed: ${reason}`), "unknown");
948
- };
949
996
  // Prevent MaxListenersExceeded warning — each iteration adds a listener
950
997
  try {
951
998
  require("events").setMaxListeners(50, signal);
952
999
  }
953
1000
  catch { /* unsupported */ }
954
1001
  const toolPreferences = currentContext?.friend?.toolPreferences;
955
- const unboundBaseTools = restrictedReactionFeedback
956
- ? (0, tools_1.getRestrictedReactionFeedbackTools)()
957
- : options?.tools ?? (0, tools_1.getToolsForChannel)(channel ? (0, friends_1.getChannelCapabilities)(channel) : undefined, toolPreferences && Object.keys(toolPreferences).length > 0 ? toolPreferences : undefined, currentContext, providerRuntime.capabilities, options?.mcpManager, providerRuntime.model);
958
- const baseTools = restrictedReactionFeedback
959
- ? unboundBaseTools
960
- : bindCurrentIngressEvidenceLocator(unboundBaseTools, options?.toolContext?.currentIngressEvidence);
1002
+ const unboundBaseTools = options?.tools ?? (0, tools_1.getToolsForChannel)(channel ? (0, friends_1.getChannelCapabilities)(channel) : undefined, toolPreferences && Object.keys(toolPreferences).length > 0 ? toolPreferences : undefined, currentContext, providerRuntime.capabilities, options?.mcpManager, providerRuntime.model);
1003
+ const baseTools = bindCurrentIngressEvidenceLocator(unboundBaseTools, options?.toolContext?.currentIngressEvidence);
961
1004
  // Augment tool context with reasoning effort controls from provider
962
1005
  const baseToolContext = options?.toolContext
963
1006
  ?? (turnOrientationFrame ? { signin: async () => undefined, orientationFrame: turnOrientationFrame } : undefined);
@@ -1001,21 +1044,17 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1001
1044
  const filteredBaseTools = isPrivateRuntimeChannel
1002
1045
  ? baseTools.filter((t) => privateRuntimeHabitCanSendMessage || t.function.name !== "send_message")
1003
1046
  : baseTools;
1004
- const activeTools = restrictedReactionFeedback
1005
- ? filteredBaseTools
1006
- : [
1007
- ...filteredBaseTools,
1008
- ...(augmentedToolContext?.noSend === true ? [] : [tools_1.ponderTool]),
1009
- ...(isPrivateRuntimeChannel && privateRuntimeHabitCanSurface ? [tools_2.surfaceToolDef] : []),
1010
- ...(isPrivateRuntimeChannel ? [tools_1.restTool] : []),
1011
- ...(!isPrivateRuntimeChannel ? [tools_1.observeTool] : []),
1012
- ...(!isPrivateRuntimeChannel ? [tools_1.settleTool] : []),
1013
- ...(isChatStyleChannel(channel ?? "") ? [tools_1.speakTool] : []),
1014
- ];
1047
+ const activeTools = [
1048
+ ...filteredBaseTools,
1049
+ ...(augmentedToolContext?.noSend === true ? [] : [tools_1.ponderTool]),
1050
+ ...(isPrivateRuntimeChannel && privateRuntimeHabitCanSurface ? [tools_2.surfaceToolDef] : []),
1051
+ ...(isPrivateRuntimeChannel ? [tools_1.restTool] : []),
1052
+ ...(!isPrivateRuntimeChannel ? [tools_1.observeTool] : []),
1053
+ ...(!isPrivateRuntimeChannel ? [tools_1.settleTool] : []),
1054
+ ...(isChatStyleChannel(channel ?? "") ? [tools_1.speakTool] : []),
1055
+ ];
1015
1056
  const activeToolNames = new Set(activeTools.map((tool) => tool.function.name));
1016
- const steeringFollowUps = restrictedReactionFeedback
1017
- ? []
1018
- : options?.drainSteeringFollowUps?.() ?? [];
1057
+ const steeringFollowUps = options?.drainSteeringFollowUps?.() ?? [];
1019
1058
  if (steeringFollowUps.length > 0) {
1020
1059
  const hasSupersedingFollowUp = steeringFollowUps.some((followUp) => followUp.effect === "clear_and_supersede");
1021
1060
  if (hasSupersedingFollowUp) {
@@ -1052,6 +1091,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1052
1091
  try {
1053
1092
  const promptBudget = (0, prompt_budget_1.applyPromptBudget)({
1054
1093
  messages,
1094
+ requiredPromptEvidence: options?.requiredPromptEvidence,
1055
1095
  provider: providerRuntime.id,
1056
1096
  model: providerRuntime.model,
1057
1097
  contextWindowTokens: (0, config_1.getContextConfig)().maxTokens,
@@ -1068,7 +1108,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1068
1108
  traceId,
1069
1109
  toolChoiceRequired,
1070
1110
  reasoningEffort: currentReasoningEffort,
1071
- eagerSettleStreaming: !restrictedReactionFeedback,
1111
+ eagerSettleStreaming: true,
1072
1112
  systemPrompt: structuredSystemPrompt,
1073
1113
  });
1074
1114
  }
@@ -1090,9 +1130,19 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1090
1130
  if (isContextOverflow(error) && !overflowRetried) {
1091
1131
  overflowRetried = true;
1092
1132
  stripLastToolCalls(messages);
1133
+ stripLastToolCalls(generatedMessages);
1093
1134
  const { maxTokens, contextMargin } = (0, config_1.getContextConfig)();
1094
1135
  const trimmed = (0, context_1.trimMessages)(messages, maxTokens, contextMargin, maxTokens * 2);
1095
- messages.splice(0, messages.length, ...trimmed);
1136
+ const requiredEvidence = options?.requiredPromptEvidence;
1137
+ const requiredMessages = new Set([
1138
+ ...(requiredEvidence?.verifiedPredecessorMessage ? [requiredEvidence.verifiedPredecessorMessage] : []),
1139
+ ...(requiredEvidence?.currentUserMessage ? [requiredEvidence.currentUserMessage] : []),
1140
+ ]);
1141
+ const trimmedMessages = new Set(trimmed);
1142
+ const overflowRetryMessages = requiredMessages.size === 0
1143
+ ? trimmed
1144
+ : messages.filter((message) => trimmedMessages.has(message) || requiredMessages.has(message));
1145
+ messages.splice(0, messages.length, ...overflowRetryMessages);
1096
1146
  providerRuntime.resetTurnState(messages);
1097
1147
  callbacks.onError(new Error("context trimmed, retrying..."), "transient");
1098
1148
  return callProviderTurn();
@@ -1104,9 +1154,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1104
1154
  operation: "turn",
1105
1155
  provider: providerRuntime.id,
1106
1156
  model: providerRuntime.model,
1107
- run: restrictedReactionFeedback ? callProviderTurn : callProviderTurnWithOverflowRecovery,
1157
+ run: callProviderTurnWithOverflowRecovery,
1108
1158
  classifyError: (error) => providerRuntime.classifyError(error),
1109
- ...(restrictedReactionFeedback ? { policy: { maxAttempts: 1 } } : {}),
1110
1159
  onRetry: async (record, maxAttempts) => {
1111
1160
  const delayMs = record.delayMs;
1112
1161
  const seconds = delayMs / 1000;
@@ -1122,7 +1171,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1122
1171
  preserveCachedOnFailure: true,
1123
1172
  providers: [record.provider],
1124
1173
  });
1125
- _providerRuntimes[facing] = null;
1174
+ _providerRuntimeFactories[facing] = null;
1126
1175
  providerRuntime = await getProviderRuntime(facing);
1127
1176
  providerRuntime.resetTurnState(messages);
1128
1177
  }
@@ -1218,100 +1267,6 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1218
1267
  if (hasPhaseAnnotation) {
1219
1268
  msg.phase = isSoleSettle ? "settle" : "commentary";
1220
1269
  }
1221
- if (restrictedReactionFeedback) {
1222
- if (result.toolCalls.length !== 1) {
1223
- streamCallbackBuffer?.discard();
1224
- finishRestrictedReactionFeedbackViolation(result.toolCalls.length === 0
1225
- ? "the provider returned no terminal tool"
1226
- : "the provider returned a mixed or multiple-tool batch");
1227
- continue;
1228
- }
1229
- const restrictedCall = result.toolCalls[0];
1230
- if (!activeToolNames.has(restrictedCall.name)) {
1231
- streamCallbackBuffer?.discard();
1232
- finishRestrictedReactionFeedbackViolation(`the provider returned unregistered tool ${JSON.stringify(restrictedCall.name)}`);
1233
- continue;
1234
- }
1235
- let restrictedArgs = null;
1236
- try {
1237
- const parsed = JSON.parse(restrictedCall.arguments);
1238
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1239
- restrictedArgs = parsed;
1240
- }
1241
- }
1242
- catch {
1243
- restrictedArgs = null;
1244
- }
1245
- const registeredTool = activeTools.find((tool) => tool.function.name === restrictedCall.name);
1246
- const argumentsMatchSchema = restrictedArgs !== null
1247
- && matchesRegisteredToolArgumentSchema(registeredTool, restrictedArgs);
1248
- const callbackArgs = (restrictedArgs ?? {});
1249
- if (restrictedCall.name === "settle") {
1250
- const { answer, intent } = parseSettlePayload(restrictedCall.arguments);
1251
- callbacks.onToolStart("settle", callbackArgs);
1252
- if (!argumentsMatchSchema || answer === undefined || intent === "direct_reply") {
1253
- callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", callbackArgs), false);
1254
- streamCallbackBuffer?.discard();
1255
- finishRestrictedReactionFeedbackViolation("settle arguments were malformed or requested continuation");
1256
- continue;
1257
- }
1258
- streamCallbackBuffer?.discard();
1259
- callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", callbackArgs), true);
1260
- const completionIntent = intent === "blocked" ? "blocked" : "complete";
1261
- completion = { answer, intent: completionIntent };
1262
- callbacks.onClearText?.();
1263
- callbacks.onTextChunk(answer);
1264
- messages.push(msg);
1265
- const delivered = "(delivered)";
1266
- messages.push({ role: "tool", tool_call_id: restrictedCall.id, content: delivered });
1267
- providerRuntime.appendToolOutput(restrictedCall.id, delivered);
1268
- outcome = completionIntent === "blocked" ? "blocked" : "settled";
1269
- done = true;
1270
- continue;
1271
- }
1272
- if (!argumentsMatchSchema) {
1273
- streamCallbackBuffer?.discard();
1274
- finishRestrictedReactionFeedbackViolation(`${restrictedCall.name} arguments were malformed`);
1275
- continue;
1276
- }
1277
- if (restrictedCall.name === "observe") {
1278
- streamCallbackBuffer?.discard();
1279
- callbacks.onClearText?.();
1280
- callbacks.onToolStart("observe", callbackArgs);
1281
- const reason = typeof callbackArgs.reason === "string" ? callbackArgs.reason : undefined;
1282
- (0, runtime_1.emitNervesEvent)({
1283
- component: "engine",
1284
- event: "engine.observe",
1285
- message: "agent observed without responding",
1286
- meta: { ...(reason ? { reason } : {}) },
1287
- });
1288
- callbacks.onToolEnd("observe", (0, tools_1.summarizeArgs)("observe", callbackArgs), true);
1289
- messages.push(msg);
1290
- const silenced = "(silenced)";
1291
- messages.push({ role: "tool", tool_call_id: restrictedCall.id, content: silenced });
1292
- providerRuntime.appendToolOutput(restrictedCall.id, silenced);
1293
- outcome = "observed";
1294
- done = true;
1295
- continue;
1296
- }
1297
- callbacks.onClearText?.();
1298
- callbacks.onToolStart("orientation_get", callbackArgs);
1299
- let orientationReadSucceeded = false;
1300
- try {
1301
- const execToolFn = options?.execTool ?? tools_1.execTool;
1302
- await execToolFn("orientation_get", callbackArgs, augmentedToolContext);
1303
- orientationReadSucceeded = true;
1304
- }
1305
- catch {
1306
- orientationReadSucceeded = false;
1307
- }
1308
- callbacks.onToolEnd("orientation_get", (0, tools_1.summarizeArgs)("orientation_get", callbackArgs), orientationReadSucceeded);
1309
- streamCallbackBuffer?.discard();
1310
- finishRestrictedReactionFeedbackViolation(orientationReadSucceeded
1311
- ? "orientation_get completed but no terminal response can follow the single invocation"
1312
- : "orientation_get failed");
1313
- continue;
1314
- }
1315
1270
  // Detect the MiniMax "only-thinking, no tool call" violation: no tool
1316
1271
  // calls returned, and the content is empty after stripping
1317
1272
  // <think>...</think> blocks. This is a narrow check — legitimate
@@ -1349,7 +1304,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1349
1304
  contentLength: result.content.length,
1350
1305
  },
1351
1306
  });
1352
- messages.push(msg);
1307
+ pushGenerated(msg);
1353
1308
  messages.push({
1354
1309
  role: "user",
1355
1310
  content: `${privateReturnTextAckRetryError} Emit the ponder(action=create, ...) tool call now, or ask a blocking clarification without saying the private work is queued.`,
@@ -1370,7 +1325,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1370
1325
  },
1371
1326
  });
1372
1327
  msg.content = blockedAnswer;
1373
- messages.push(msg);
1328
+ pushGenerated(msg);
1374
1329
  callbacks.onTextChunk(blockedAnswer);
1375
1330
  completion = { answer: blockedAnswer, intent: "blocked" };
1376
1331
  outcome = "blocked";
@@ -1398,7 +1353,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1398
1353
  contentLength: result.content.length,
1399
1354
  },
1400
1355
  });
1401
- messages.push(msg);
1356
+ pushGenerated(msg);
1402
1357
  messages.push({
1403
1358
  role: "user",
1404
1359
  content: isPrivateRuntimeChannel
@@ -1411,7 +1366,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1411
1366
  }
1412
1367
  // Legitimate text-only response, or cap reached — accept as-is.
1413
1368
  await streamCallbackBuffer?.flush();
1414
- messages.push(msg);
1369
+ pushGenerated(msg);
1415
1370
  done = true;
1416
1371
  }
1417
1372
  else {
@@ -1421,10 +1376,10 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1421
1376
  if (habitBlockReason) {
1422
1377
  streamCallbackBuffer?.discard();
1423
1378
  recordBlockedHabitSurfaceAttempts(habitSession, result.toolCalls, habitBlockReason);
1424
- messages.push(msg);
1379
+ pushGenerated(msg);
1425
1380
  const blockedOutput = `blocked: ${habitBlockReason}. No tool side effects from this assistant message were executed.`;
1426
1381
  for (const call of result.toolCalls) {
1427
- messages.push({ role: "tool", tool_call_id: call.id, content: blockedOutput });
1382
+ pushGenerated({ role: "tool", tool_call_id: call.id, content: blockedOutput });
1428
1383
  providerRuntime.appendToolOutput(call.id, blockedOutput);
1429
1384
  }
1430
1385
  (0, runtime_1.emitNervesEvent)({
@@ -1470,9 +1425,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1470
1425
  }
1471
1426
  catch (error) {
1472
1427
  callbacks.onToolEnd(soleTerminalCall.name, (0, tools_1.summarizeArgs)(soleTerminalCall.name, terminalArgs), false);
1473
- messages.push(msg);
1428
+ pushGenerated(msg);
1474
1429
  const failure = error instanceof Error ? `error: ${error.message}` : `error: ${String(error)}`;
1475
- messages.push({ role: "tool", tool_call_id: soleTerminalCall.id, content: failure });
1430
+ pushGenerated({ role: "tool", tool_call_id: soleTerminalCall.id, content: failure });
1476
1431
  providerRuntime.appendToolOutput(soleTerminalCall.id, failure);
1477
1432
  callbacks.onTextChunk(failure);
1478
1433
  completion = { answer: failure, intent: "blocked" };
@@ -1481,8 +1436,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1481
1436
  continue;
1482
1437
  }
1483
1438
  callbacks.onToolEnd(soleTerminalCall.name, (0, tools_1.summarizeArgs)(soleTerminalCall.name, terminalArgs), true);
1484
- messages.push(msg);
1485
- messages.push({ role: "tool", tool_call_id: soleTerminalCall.id, content: terminalResult });
1439
+ pushGenerated(msg);
1440
+ pushGenerated({ role: "tool", tool_call_id: soleTerminalCall.id, content: terminalResult });
1486
1441
  providerRuntime.appendToolOutput(soleTerminalCall.id, terminalResult);
1487
1442
  callbacks.onTextChunk(terminalResult);
1488
1443
  completion = { answer: terminalResult, intent: "complete" };
@@ -1506,9 +1461,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1506
1461
  streamCallbackBuffer?.discard();
1507
1462
  callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
1508
1463
  callbacks.onClearText?.();
1509
- messages.push(msg);
1464
+ pushGenerated(msg);
1510
1465
  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.";
1511
- messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: gateMessage });
1466
+ pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: gateMessage });
1512
1467
  providerRuntime.appendToolOutput(result.toolCalls[0].id, gateMessage);
1513
1468
  continue;
1514
1469
  }
@@ -1519,9 +1474,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1519
1474
  if (isPrivateRuntimeChannel) {
1520
1475
  streamCallbackBuffer?.discard();
1521
1476
  callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), true);
1522
- messages.push(msg);
1477
+ pushGenerated(msg);
1523
1478
  const settled = "(settled)";
1524
- messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: settled });
1479
+ pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: settled });
1525
1480
  providerRuntime.appendToolOutput(result.toolCalls[0].id, settled);
1526
1481
  outcome = "settled";
1527
1482
  done = true;
@@ -1559,15 +1514,15 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1559
1514
  };
1560
1515
  // Retractable owners already hold the validated answer. Final-only
1561
1516
  // owners receive it here, after every semantic continuation gate.
1562
- messages.push(msg);
1517
+ pushGenerated(msg);
1563
1518
  if (validDirectReply) {
1564
1519
  const resumeWork = "direct reply delivered. resume the unresolved obligation now and keep working until you can finish or clearly report that you are blocked.";
1565
- messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: resumeWork });
1520
+ pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: resumeWork });
1566
1521
  providerRuntime.appendToolOutput(result.toolCalls[0].id, resumeWork);
1567
1522
  }
1568
1523
  else {
1569
1524
  const delivered = "(delivered)";
1570
- messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: delivered });
1525
+ pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: delivered });
1571
1526
  providerRuntime.appendToolOutput(result.toolCalls[0].id, delivered);
1572
1527
  outcome = intent === "blocked" ? "blocked" : "settled";
1573
1528
  done = true;
@@ -1579,9 +1534,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1579
1534
  streamCallbackBuffer?.discard();
1580
1535
  callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
1581
1536
  callbacks.onClearText?.();
1582
- messages.push(msg);
1537
+ pushGenerated(msg);
1583
1538
  const toolRetryMessage = retryError;
1584
- messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: toolRetryMessage });
1539
+ pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: toolRetryMessage });
1585
1540
  providerRuntime.appendToolOutput(result.toolCalls[0].id, toolRetryMessage);
1586
1541
  }
1587
1542
  continue;
@@ -1608,9 +1563,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1608
1563
  meta: { ...(reason ? { reason } : {}) },
1609
1564
  });
1610
1565
  callbacks.onToolEnd("observe", (0, tools_1.summarizeArgs)("observe", observeArgs), true);
1611
- messages.push(msg);
1566
+ pushGenerated(msg);
1612
1567
  const silenced = "(silenced)";
1613
- messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: silenced });
1568
+ pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: silenced });
1614
1569
  providerRuntime.appendToolOutput(result.toolCalls[0].id, silenced);
1615
1570
  outcome = "observed";
1616
1571
  done = true;
@@ -1631,18 +1586,18 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1631
1586
  const attentionQueue = augmentedToolContext?.delegatedOrigins;
1632
1587
  if (attentionQueue && attentionQueue.length > 0) {
1633
1588
  callbacks.onToolEnd("rest", (0, tools_1.summarizeArgs)("rest", restArgs), false);
1634
- messages.push(msg);
1589
+ pushGenerated(msg);
1635
1590
  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.";
1636
- messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: gateMessage });
1591
+ pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: gateMessage });
1637
1592
  providerRuntime.appendToolOutput(result.toolCalls[0].id, gateMessage);
1638
1593
  continue;
1639
1594
  }
1640
1595
  if (hasFreshPendingWork(options) && !freshWorkGateFired) {
1641
1596
  freshWorkGateFired = true;
1642
1597
  callbacks.onToolEnd("rest", (0, tools_1.summarizeArgs)("rest", restArgs), false);
1643
- messages.push(msg);
1598
+ pushGenerated(msg);
1644
1599
  const gateMessage = "fresh work arrived for me this turn — inspect the pending messages above and take the next concrete action before you rest.";
1645
- messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: gateMessage });
1600
+ pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: gateMessage });
1646
1601
  providerRuntime.appendToolOutput(result.toolCalls[0].id, gateMessage);
1647
1602
  (0, runtime_1.emitNervesEvent)({
1648
1603
  level: "info",
@@ -1654,9 +1609,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1654
1609
  continue;
1655
1610
  }
1656
1611
  callbacks.onToolEnd("rest", (0, tools_1.summarizeArgs)("rest", restArgs), true);
1657
- messages.push(msg);
1612
+ pushGenerated(msg);
1658
1613
  const ack = "(resting)";
1659
- messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: ack });
1614
+ pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: ack });
1660
1615
  providerRuntime.appendToolOutput(result.toolCalls[0].id, ack);
1661
1616
  (0, runtime_1.emitNervesEvent)({
1662
1617
  component: "engine",
@@ -1680,11 +1635,26 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1680
1635
  else {
1681
1636
  await streamCallbackBuffer?.flush();
1682
1637
  }
1683
- messages.push(msg);
1638
+ pushGenerated(msg);
1684
1639
  // Execute tools (sole-call tools in mixed calls are rejected inline)
1685
1640
  for (const tc of result.toolCalls) {
1686
1641
  if (signal?.aborted)
1687
1642
  break;
1643
+ if (tc.name === "speak" && !activeToolNames.has("speak")) {
1644
+ const rejection = "rejected: speak was not advertised for this channel; no outward message was sent.";
1645
+ callbacks.onToolStart(tc.name, {});
1646
+ callbacks.onToolEnd(tc.name, "", false);
1647
+ pushGenerated({ role: "tool", tool_call_id: tc.id, content: rejection });
1648
+ providerRuntime.appendToolOutput(tc.id, rejection);
1649
+ (0, runtime_1.emitNervesEvent)({
1650
+ level: "warn",
1651
+ component: "engine",
1652
+ event: "engine.unadvertised_speak_blocked",
1653
+ message: "blocked an unadvertised speak tool call",
1654
+ meta: { channel: String(channel) },
1655
+ });
1656
+ continue;
1657
+ }
1688
1658
  // Reject sole-call tools when mixed with other tool calls
1689
1659
  const terminalProjection = (0, tools_1.resolveToolDefinition)(tc.name)?.terminalProjection;
1690
1660
  const soleCallRejection = SOLE_CALL_REJECTION[tc.name]
@@ -1692,7 +1662,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1692
1662
  ? `rejected: ${tc.name} must be the only tool call.`
1693
1663
  : undefined);
1694
1664
  if (soleCallRejection) {
1695
- messages.push({ role: "tool", tool_call_id: tc.id, content: soleCallRejection });
1665
+ pushGenerated({ role: "tool", tool_call_id: tc.id, content: soleCallRejection });
1696
1666
  providerRuntime.appendToolOutput(tc.id, soleCallRejection);
1697
1667
  continue;
1698
1668
  }
@@ -1710,7 +1680,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1710
1680
  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.";
1711
1681
  callbacks.onToolStart(tc.name, args);
1712
1682
  callbacks.onToolEnd(tc.name, argSummary, false);
1713
- messages.push({ role: "tool", tool_call_id: tc.id, content: rejection });
1683
+ pushGenerated({ role: "tool", tool_call_id: tc.id, content: rejection });
1714
1684
  providerRuntime.appendToolOutput(tc.id, rejection);
1715
1685
  continue;
1716
1686
  }
@@ -1728,7 +1698,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1728
1698
  if (speakMessage.trim().length === 0) {
1729
1699
  const err = "speak requires a non-empty `message` string.";
1730
1700
  callbacks.onToolEnd("speak", argSummary, false);
1731
- messages.push({ role: "tool", tool_call_id: tc.id, content: err });
1701
+ pushGenerated({ role: "tool", tool_call_id: tc.id, content: err });
1732
1702
  providerRuntime.appendToolOutput(tc.id, err);
1733
1703
  (0, runtime_1.emitNervesEvent)({
1734
1704
  level: "warn",
@@ -1750,7 +1720,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1750
1720
  if (speakDeliveryError) {
1751
1721
  callbacks.onToolEnd("speak", argSummary, false);
1752
1722
  const failMsg = `speak delivery failed: ${speakDeliveryError.message}. the message did not reach your friend; do not assume they saw it.`;
1753
- messages.push({ role: "tool", tool_call_id: tc.id, content: failMsg });
1723
+ pushGenerated({ role: "tool", tool_call_id: tc.id, content: failMsg });
1754
1724
  providerRuntime.appendToolOutput(tc.id, failMsg);
1755
1725
  (0, runtime_1.emitNervesEvent)({
1756
1726
  level: "error",
@@ -1763,7 +1733,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1763
1733
  }
1764
1734
  callbacks.onToolEnd("speak", argSummary, true);
1765
1735
  const ack = "(spoken)";
1766
- messages.push({ role: "tool", tool_call_id: tc.id, content: ack });
1736
+ pushGenerated({ role: "tool", tool_call_id: tc.id, content: ack });
1767
1737
  providerRuntime.appendToolOutput(tc.id, ack);
1768
1738
  (0, runtime_1.emitNervesEvent)({
1769
1739
  component: "engine",
@@ -1928,7 +1898,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1928
1898
  toolResult = error instanceof Error ? error.message : String(error);
1929
1899
  }
1930
1900
  callbacks.onToolEnd(tc.name, argSummary, success);
1931
- messages.push({ role: "tool", tool_call_id: tc.id, content: toolResult });
1901
+ pushGenerated({ role: "tool", tool_call_id: tc.id, content: toolResult });
1932
1902
  providerRuntime.appendToolOutput(tc.id, toolResult);
1933
1903
  continue;
1934
1904
  }
@@ -1947,7 +1917,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1947
1917
  const rejection = `loop guard: ${toolLoop.message}`;
1948
1918
  callbacks.onToolStart(tc.name, args);
1949
1919
  callbacks.onToolEnd(tc.name, argSummary, false);
1950
- messages.push({ role: "tool", tool_call_id: tc.id, content: rejection });
1920
+ pushGenerated({ role: "tool", tool_call_id: tc.id, content: rejection });
1951
1921
  providerRuntime.appendToolOutput(tc.id, rejection);
1952
1922
  continue;
1953
1923
  }
@@ -1967,7 +1937,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1967
1937
  toolResult = (0, tool_friction_1.rewriteToolResultForModel)(tc.name, toolResult, toolFrictionLedger);
1968
1938
  (0, tool_loop_1.recordToolOutcome)(toolLoopState, tc.name, args, toolResult, success);
1969
1939
  callbacks.onToolEnd(tc.name, (0, tools_1.buildToolResultSummary)(tc.name, args, toolResult, success), success);
1970
- messages.push({ role: "tool", tool_call_id: tc.id, content: toolResult });
1940
+ pushGenerated({ role: "tool", tool_call_id: tc.id, content: toolResult });
1971
1941
  providerRuntime.appendToolOutput(tc.id, toolResult);
1972
1942
  callbacks.onToolResult?.(messages);
1973
1943
  }
@@ -1977,6 +1947,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1977
1947
  // Abort is not an error — just stop cleanly
1978
1948
  if (e instanceof provider_attempt_1.ProviderAttemptAbortError || signal?.aborted) {
1979
1949
  stripLastToolCalls(messages);
1950
+ stripLastToolCalls(generatedMessages);
1980
1951
  outcome = "aborted";
1981
1952
  break;
1982
1953
  }
@@ -1992,6 +1963,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1992
1963
  finishTerminalProviderError(errorForClassification, providerClassification);
1993
1964
  }
1994
1965
  }
1966
+ options?.captureGeneratedMessages?.(structuredClone(generatedMessages));
1995
1967
  (0, runtime_1.emitNervesEvent)({
1996
1968
  event: "engine.turn_end",
1997
1969
  trace_id: traceId,