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

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 (54) hide show
  1. package/changelog.json +18 -0
  2. package/dist/heart/active-work.js +10 -3
  3. package/dist/heart/core.js +343 -77
  4. package/dist/heart/daemon/cli-exec.js +72 -14
  5. package/dist/heart/daemon/cli-help.js +13 -2
  6. package/dist/heart/daemon/cli-parse.js +82 -1
  7. package/dist/heart/daemon/daemon.js +286 -33
  8. package/dist/heart/daemon/doctor.js +21 -6
  9. package/dist/heart/daemon/habit-private-wake.js +6 -1
  10. package/dist/heart/habits/habit-cancel.js +810 -0
  11. package/dist/heart/habits/habit-lifecycle.js +1327 -0
  12. package/dist/heart/habits/habit-migration.js +28 -23
  13. package/dist/heart/habits/habit-parser.js +72 -8
  14. package/dist/heart/habits/habit-runtime-state.js +3 -24
  15. package/dist/heart/habits/habit-scheduler.js +268 -50
  16. package/dist/heart/habits/habit-session.js +16 -0
  17. package/dist/heart/hatch/hatch-flow.js +6 -4
  18. package/dist/heart/mailbox/readers/runtime-readers.js +26 -27
  19. package/dist/heart/orientation-frame.js +58 -1
  20. package/dist/heart/provider-attempt.js +4 -1
  21. package/dist/heart/providers/anthropic.js +23 -2
  22. package/dist/heart/start-of-turn-packet.js +14 -11
  23. package/dist/heart/streaming.js +565 -185
  24. package/dist/mind/obligation-steering.js +15 -9
  25. package/dist/mind/prompt.js +58 -26
  26. package/dist/nerves/coverage/run-artifacts.js +55 -35
  27. package/dist/nerves/review/cli.js +2 -2
  28. package/dist/nerves/review/core.js +14 -7
  29. package/dist/repertoire/tools-base.js +2 -0
  30. package/dist/repertoire/tools-habits.js +103 -0
  31. package/dist/repertoire/tools.js +18 -3
  32. package/dist/rsvp/cli.js +126 -20
  33. package/dist/rsvp/diagnostics.js +12 -2
  34. package/dist/rsvp/habit-policy.js +7 -0
  35. package/dist/rsvp/habit-stage.js +19 -4
  36. package/dist/rsvp/native-habit-runner.js +151 -11
  37. package/dist/rsvp/outbound-state.js +360 -0
  38. package/dist/senses/bluebubbles/client.js +90 -17
  39. package/dist/senses/bluebubbles/inbound-log.js +1 -0
  40. package/dist/senses/bluebubbles/index.js +662 -425
  41. package/dist/senses/bluebubbles/model.js +57 -13
  42. package/dist/senses/bluebubbles/mutation-log.js +21 -10
  43. package/dist/senses/bluebubbles/reaction-policy.js +55 -0
  44. package/dist/senses/bluebubbles/semantic-receipts.js +1818 -0
  45. package/dist/senses/cli/tui-store.js +1 -0
  46. package/dist/senses/cli.js +1 -0
  47. package/dist/senses/habit-lifecycle-guard.js +24 -0
  48. package/dist/senses/ingress-evidence.js +2 -0
  49. package/dist/senses/pipeline.js +90 -24
  50. package/dist/senses/private-runtime-worker.js +21 -20
  51. package/dist/senses/private-runtime.js +79 -29
  52. package/dist/senses/shared-turn.js +1 -0
  53. package/dist/senses/teams.js +1 -0
  54. package/package.json +6 -1
@@ -19,6 +19,7 @@ const identity_1 = require("./identity");
19
19
  const tools_1 = require("../repertoire/tools");
20
20
  const friends_1 = require("@ouro.bot/friends");
21
21
  const tools_2 = require("../repertoire/tools");
22
+ const streaming_1 = require("./streaming");
22
23
  const runtime_1 = require("../nerves/runtime");
23
24
  const context_1 = require("../mind/context");
24
25
  const prompt_budget_1 = require("../mind/prompt-budget");
@@ -30,6 +31,7 @@ const azure_1 = require("./providers/azure");
30
31
  const minimax_1 = require("./providers/minimax");
31
32
  const openai_codex_1 = require("./providers/openai-codex");
32
33
  const github_copilot_1 = require("./providers/github-copilot");
34
+ const orientation_frame_1 = require("./orientation-frame");
33
35
  const identity_2 = require("./identity");
34
36
  const socket_client_1 = require("./daemon/socket-client");
35
37
  const obligations_1 = require("../arc/obligations");
@@ -188,17 +190,20 @@ function getProviderDisplayLabel(facing = "human") {
188
190
  };
189
191
  return providerLabelBuilders[provider]();
190
192
  }
191
- function createHabitCallbackBuffer(callbacks) {
193
+ function createCallbackBuffer(callbacks, mode) {
192
194
  const events = [];
195
+ const bufferedCallbacks = {
196
+ ...callbacks,
197
+ onTextChunk: (text) => { events.push({ kind: "text", text }); },
198
+ onClearText: () => { events.push({ kind: "clear" }); },
199
+ flushNow: () => { events.push({ kind: "flush" }); },
200
+ };
201
+ if (mode === "all") {
202
+ bufferedCallbacks.onModelStreamStart = () => { events.push({ kind: "stream-start" }); };
203
+ bufferedCallbacks.onReasoningChunk = (text) => { events.push({ kind: "reasoning", text }); };
204
+ }
193
205
  return {
194
- callbacks: {
195
- ...callbacks,
196
- onModelStreamStart: () => { events.push({ kind: "stream-start" }); },
197
- onTextChunk: (text) => { events.push({ kind: "text", text }); },
198
- onReasoningChunk: (text) => { events.push({ kind: "reasoning", text }); },
199
- onClearText: () => { events.push({ kind: "clear" }); },
200
- flushNow: () => { events.push({ kind: "flush" }); },
201
- },
206
+ callbacks: bufferedCallbacks,
202
207
  async flush() {
203
208
  for (const event of events.splice(0)) {
204
209
  switch (event.kind) {
@@ -225,6 +230,12 @@ function createHabitCallbackBuffer(callbacks) {
225
230
  },
226
231
  };
227
232
  }
233
+ function createHabitCallbackBuffer(callbacks) {
234
+ return createCallbackBuffer(callbacks, "all");
235
+ }
236
+ function createFinalOnlyTextBuffer(callbacks) {
237
+ return createCallbackBuffer(callbacks, "text-only");
238
+ }
228
239
  /**
229
240
  * Strip <think>...</think> blocks for the violation-detection check at the
230
241
  * end of a streaming turn. Used to tell legitimate text-only responses
@@ -295,7 +306,10 @@ function recordBlockedHabitSurfaceAttempts(habitSession, toolCalls, reason) {
295
306
  });
296
307
  }
297
308
  }
298
- async function habitToolBatchBlockReason(habitSession, toolCalls, delegatedOrigins, activeToolNames) {
309
+ async function habitToolBatchBlockReason(habitSession, toolCalls, delegatedOrigins, activeToolNames, noSend) {
310
+ if (noSend && toolCalls.some((call) => call.name === "ponder")) {
311
+ return "habit tool 'ponder' cannot create continuations under immutable no-send authority";
312
+ }
299
313
  if (!habitSession)
300
314
  return null;
301
315
  const granted = new Set(habitSession.toolPolicy.grantedTools);
@@ -340,6 +354,36 @@ async function habitToolBatchBlockReason(habitSession, toolCalls, delegatedOrigi
340
354
  function isChatStyleChannel(channel) {
341
355
  return channel === "cli" || channel === "teams" || channel === "bluebubbles" || channel === "voice";
342
356
  }
357
+ function bindCurrentIngressEvidenceLocator(toolList, evidence) {
358
+ if (!evidence
359
+ || evidence.schemaVersion !== 1
360
+ || evidence.provider !== "bluebubbles"
361
+ || !/^[a-f0-9]{64}$/.test(evidence.captureKeyHash))
362
+ return toolList;
363
+ const locator = `capture:${evidence.captureKeyHash}`;
364
+ return toolList.map((tool) => {
365
+ if (tool.function.name !== "habit_cancel")
366
+ return tool;
367
+ const parameters = tool.function.parameters;
368
+ const properties = parameters.properties;
369
+ return {
370
+ ...tool,
371
+ function: {
372
+ ...tool.function,
373
+ parameters: {
374
+ ...parameters,
375
+ properties: {
376
+ ...properties,
377
+ evidence: {
378
+ ...properties.evidence,
379
+ enum: [locator],
380
+ },
381
+ },
382
+ },
383
+ },
384
+ };
385
+ });
386
+ }
343
387
  // Sole-call tools must be the only tool call in a turn. When they appear
344
388
  // alongside other tools, the sole-call tool is rejected with this message.
345
389
  const SOLE_CALL_REJECTION = {
@@ -350,14 +394,8 @@ const SOLE_CALL_REJECTION = {
350
394
  function parseSettlePayload(argumentsText) {
351
395
  try {
352
396
  const parsed = JSON.parse(argumentsText);
353
- if (typeof parsed === "string") {
354
- return { answer: parsed };
355
- }
356
- if (!parsed || typeof parsed !== "object") {
357
- return {};
358
- }
359
- const answer = typeof parsed.answer === "string" ? parsed.answer : undefined;
360
- const rawIntent = parsed.intent;
397
+ const answer = typeof parsed?.answer === "string" ? parsed.answer : undefined;
398
+ const rawIntent = parsed?.intent;
361
399
  const intent = rawIntent === "complete" || rawIntent === "blocked" || rawIntent === "direct_reply"
362
400
  ? rawIntent
363
401
  : undefined;
@@ -367,6 +405,23 @@ function parseSettlePayload(argumentsText) {
367
405
  return {};
368
406
  }
369
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;
424
+ }
370
425
  function parsePonderPayload(argumentsText) {
371
426
  try {
372
427
  const parsed = JSON.parse(argumentsText);
@@ -574,6 +629,15 @@ function upsertSystemPrompt(messages, systemText) {
574
629
  messages.unshift(systemMessage);
575
630
  }
576
631
  }
632
+ /**
633
+ * A prompt refresh failure must not revive any generated text from an older
634
+ * turn or release. Even the nominally stable region contains actor-scoped trust,
635
+ * tool, and channel context, so it is not safe to reconstruct. Fail closed to a
636
+ * neutral base and put the freshly derived trigger first.
637
+ */
638
+ function repairFallbackSystemPrompt(orientationFrame) {
639
+ return `${(0, orientation_frame_1.renderOrientationFrame)(orientationFrame)}\n\nYou are a helpful assistant.`;
640
+ }
577
641
  // Remove orphan tool_calls from the last assistant message and any
578
642
  // trailing tool-result messages that lack a matching tool_call.
579
643
  // This keeps the conversation valid after an abort or tool-loop limit.
@@ -747,7 +811,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
747
811
  const facing = (0, friends_1.channelToFacing)(channel);
748
812
  let providerRuntime = await getProviderRuntime(facing);
749
813
  const provider = providerRuntime.id;
750
- const toolChoiceRequired = options?.toolChoiceRequired ?? true;
814
+ const restrictedReactionFeedback = options?.restrictedReactionFeedback === true;
815
+ const toolChoiceRequired = restrictedReactionFeedback ? true : options?.toolChoiceRequired ?? true;
751
816
  const traceId = options?.traceId;
752
817
  (0, runtime_1.emitNervesEvent)({
753
818
  event: "engine.turn_start",
@@ -766,14 +831,18 @@ async function runAgent(messages, callbacks, channel, signal, options) {
766
831
  currentContext = { ...currentContext, friend: freshFriend };
767
832
  }
768
833
  }
834
+ const turnOrientationFrame = options?.orientationFrame
835
+ ?? (channel ? (0, orientation_frame_1.buildOrientationFrame)({ channel, messages }) : undefined);
769
836
  // Refresh system prompt at start of each turn when channel is provided.
770
- // If refresh fails, keep existing system prompt (or inject a minimal safe fallback)
771
- // so turn execution remains consistent and non-fatal.
837
+ // If refresh fails, retain only a recognised stable prefix (or inject a
838
+ // minimal safe fallback) so stale dynamic state cannot regain authority.
772
839
  let structuredSystemPrompt;
773
840
  if (channel) {
774
841
  try {
775
842
  const buildSystemOptions = {
776
843
  ...options,
844
+ orientationFrame: turnOrientationFrame,
845
+ resumePriorWork: options?.resumePriorWork === true,
777
846
  providerCapabilities: providerRuntime.capabilities,
778
847
  supportedReasoningEfforts: providerRuntime.supportedReasoningEfforts,
779
848
  };
@@ -783,8 +852,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
783
852
  }
784
853
  catch (error) {
785
854
  const hadExistingSystemPrompt = messages[0]?.role === "system" && typeof messages[0].content === "string";
786
- const existingSystemText = hadExistingSystemPrompt ? messages[0].content : undefined;
787
- const fallback = existingSystemText ?? "You are a helpful assistant.";
855
+ const fallback = repairFallbackSystemPrompt(turnOrientationFrame);
788
856
  upsertSystemPrompt(messages, fallback);
789
857
  (0, runtime_1.emitNervesEvent)({
790
858
  level: "warn",
@@ -874,16 +942,25 @@ async function runAgent(messages, callbacks, channel, signal, options) {
874
942
  outcome = "errored";
875
943
  done = true;
876
944
  };
945
+ const finishRestrictedReactionFeedbackViolation = (reason) => {
946
+ callbacks.onClearText?.();
947
+ finishTerminalProviderError(new Error(`restricted reaction feedback failed closed: ${reason}`), "unknown");
948
+ };
877
949
  // Prevent MaxListenersExceeded warning — each iteration adds a listener
878
950
  try {
879
951
  require("events").setMaxListeners(50, signal);
880
952
  }
881
953
  catch { /* unsupported */ }
882
954
  const toolPreferences = currentContext?.friend?.toolPreferences;
883
- const baseTools = 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);
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);
884
961
  // Augment tool context with reasoning effort controls from provider
885
962
  const baseToolContext = options?.toolContext
886
- ?? (options?.orientationFrame ? { signin: async () => undefined, orientationFrame: options.orientationFrame } : undefined);
963
+ ?? (turnOrientationFrame ? { signin: async () => undefined, orientationFrame: turnOrientationFrame } : undefined);
887
964
  const habitSession = options?.habitSession ?? baseToolContext?.habitSession;
888
965
  const augmentedToolContext = baseToolContext
889
966
  ? {
@@ -891,7 +968,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
891
968
  supportedReasoningEfforts: providerRuntime.supportedReasoningEfforts,
892
969
  setReasoningEffort: (level) => { currentReasoningEffort = level; },
893
970
  activeWorkFrame: options?.activeWorkFrame,
894
- orientationFrame: options?.orientationFrame ?? baseToolContext.orientationFrame,
971
+ orientationFrame: turnOrientationFrame ?? baseToolContext.orientationFrame,
895
972
  ...(habitSession ? { habitSession } : {}),
896
973
  }
897
974
  : habitSession
@@ -924,17 +1001,21 @@ async function runAgent(messages, callbacks, channel, signal, options) {
924
1001
  const filteredBaseTools = isPrivateRuntimeChannel
925
1002
  ? baseTools.filter((t) => privateRuntimeHabitCanSendMessage || t.function.name !== "send_message")
926
1003
  : baseTools;
927
- const activeTools = [
928
- ...filteredBaseTools,
929
- tools_1.ponderTool,
930
- ...(isPrivateRuntimeChannel && privateRuntimeHabitCanSurface ? [tools_2.surfaceToolDef] : []),
931
- ...(isPrivateRuntimeChannel ? [tools_1.restTool] : []),
932
- ...(!isPrivateRuntimeChannel ? [tools_1.observeTool] : []),
933
- ...(!isPrivateRuntimeChannel ? [tools_1.settleTool] : []),
934
- ...(isChatStyleChannel(channel ?? "") ? [tools_1.speakTool] : []),
935
- ];
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
+ ];
936
1015
  const activeToolNames = new Set(activeTools.map((tool) => tool.function.name));
937
- const steeringFollowUps = options?.drainSteeringFollowUps?.() ?? [];
1016
+ const steeringFollowUps = restrictedReactionFeedback
1017
+ ? []
1018
+ : options?.drainSteeringFollowUps?.() ?? [];
938
1019
  if (steeringFollowUps.length > 0) {
939
1020
  const hasSupersedingFollowUp = steeringFollowUps.some((followUp) => followUp.effect === "clear_and_supersede");
940
1021
  if (hasSupersedingFollowUp) {
@@ -960,10 +1041,14 @@ async function runAgent(messages, callbacks, channel, signal, options) {
960
1041
  break;
961
1042
  }
962
1043
  try {
963
- const habitCallbackBufferRef = { current: null };
1044
+ const turnCallbackBufferRef = { current: null };
964
1045
  const callProviderTurn = async () => {
965
1046
  callbacks.onModelStart();
966
- habitCallbackBufferRef.current = habitSession ? createHabitCallbackBuffer(callbacks) : null;
1047
+ turnCallbackBufferRef.current = habitSession
1048
+ ? createHabitCallbackBuffer(callbacks)
1049
+ : callbacks.settleOutputMode === "final_only"
1050
+ ? createFinalOnlyTextBuffer(callbacks)
1051
+ : null;
967
1052
  try {
968
1053
  const promptBudget = (0, prompt_budget_1.applyPromptBudget)({
969
1054
  messages,
@@ -978,18 +1063,18 @@ async function runAgent(messages, callbacks, channel, signal, options) {
978
1063
  return await providerRuntime.streamTurn({
979
1064
  messages,
980
1065
  activeTools,
981
- callbacks: habitCallbackBufferRef.current?.callbacks ?? callbacks,
1066
+ callbacks: turnCallbackBufferRef.current?.callbacks ?? callbacks,
982
1067
  signal,
983
1068
  traceId,
984
1069
  toolChoiceRequired,
985
1070
  reasoningEffort: currentReasoningEffort,
986
- eagerSettleStreaming: true,
1071
+ eagerSettleStreaming: !restrictedReactionFeedback,
987
1072
  systemPrompt: structuredSystemPrompt,
988
1073
  });
989
1074
  }
990
1075
  catch (error) {
991
- habitCallbackBufferRef.current?.discard();
992
- habitCallbackBufferRef.current = null;
1076
+ turnCallbackBufferRef.current?.discard();
1077
+ turnCallbackBufferRef.current = null;
993
1078
  if (signal?.aborted)
994
1079
  throw new provider_attempt_1.ProviderAttemptAbortError();
995
1080
  throw error;
@@ -1019,8 +1104,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1019
1104
  operation: "turn",
1020
1105
  provider: providerRuntime.id,
1021
1106
  model: providerRuntime.model,
1022
- run: callProviderTurnWithOverflowRecovery,
1107
+ run: restrictedReactionFeedback ? callProviderTurn : callProviderTurnWithOverflowRecovery,
1023
1108
  classifyError: (error) => providerRuntime.classifyError(error),
1109
+ ...(restrictedReactionFeedback ? { policy: { maxAttempts: 1 } } : {}),
1024
1110
  onRetry: async (record, maxAttempts) => {
1025
1111
  const delayMs = record.delayMs;
1026
1112
  const seconds = delayMs / 1000;
@@ -1061,8 +1147,17 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1061
1147
  continue;
1062
1148
  }
1063
1149
  const result = attempt.value;
1064
- const streamCallbackBuffer = habitCallbackBufferRef.current;
1065
- habitCallbackBufferRef.current = null;
1150
+ const streamCallbackBuffer = turnCallbackBufferRef.current;
1151
+ turnCallbackBufferRef.current = null;
1152
+ if (result.settleFinalization && !result.settleFinalization.ok) {
1153
+ // A completed settle payload is terminal provider output, not a prompt
1154
+ // for another model turn. Retractable callbacks have already cleared;
1155
+ // core-owned callback buffers must be discarded before surfacing the exact
1156
+ // parser failure through the ordinary terminal-error path.
1157
+ streamCallbackBuffer?.discard();
1158
+ finishTerminalProviderError(new Error(result.settleFinalization.errorCode), "unknown");
1159
+ continue;
1160
+ }
1066
1161
  // Track usage from the latest API call
1067
1162
  if (result.usage)
1068
1163
  lastUsage = result.usage;
@@ -1123,6 +1218,100 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1123
1218
  if (hasPhaseAnnotation) {
1124
1219
  msg.phase = isSoleSettle ? "settle" : "commentary";
1125
1220
  }
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
+ }
1126
1315
  // Detect the MiniMax "only-thinking, no tool call" violation: no tool
1127
1316
  // calls returned, and the content is empty after stripping
1128
1317
  // <think>...</think> blocks. This is a narrow check — legitimate
@@ -1141,8 +1330,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1141
1330
  })
1142
1331
  : null;
1143
1332
  if (!result.toolCalls.length) {
1144
- await streamCallbackBuffer?.flush();
1145
1333
  if (privateReturnTextAckRetryError) {
1334
+ streamCallbackBuffer?.discard();
1146
1335
  callbacks.onClearText?.();
1147
1336
  if (noToolCallRetries < NO_TOOL_CALL_MAX_RETRIES) {
1148
1337
  noToolCallRetries++;
@@ -1189,6 +1378,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1189
1378
  continue;
1190
1379
  }
1191
1380
  if (onlyThinkContent && toolChoiceRequired && noToolCallRetries < NO_TOOL_CALL_MAX_RETRIES) {
1381
+ streamCallbackBuffer?.discard();
1192
1382
  // Provider-level violation: tool_choice was required, model emitted
1193
1383
  // only a <think>...</think> block (or empty content) with no tool
1194
1384
  // call. Retry with a corrective nudge up to NO_TOOL_CALL_MAX_RETRIES
@@ -1212,19 +1402,22 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1212
1402
  messages.push({
1213
1403
  role: "user",
1214
1404
  content: isPrivateRuntimeChannel
1215
- ? "no tool was called this turn. you must end every turn by calling rest (or surface, ponder, observe). emit the tool call now."
1405
+ ? augmentedToolContext?.noSend === true
1406
+ ? "no tool was called this turn. this is an immutable no-send turn; call rest now without creating a continuation."
1407
+ : "no tool was called this turn. you must end every turn by calling rest (or surface, ponder, observe). emit the tool call now."
1216
1408
  : "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.",
1217
1409
  });
1218
1410
  continue;
1219
1411
  }
1220
1412
  // Legitimate text-only response, or cap reached — accept as-is.
1413
+ await streamCallbackBuffer?.flush();
1221
1414
  messages.push(msg);
1222
1415
  done = true;
1223
1416
  }
1224
1417
  else {
1225
1418
  // Reset the retry counter on any successful tool call.
1226
1419
  noToolCallRetries = 0;
1227
- const habitBlockReason = await habitToolBatchBlockReason(habitSession, result.toolCalls, augmentedToolContext?.delegatedOrigins, activeToolNames);
1420
+ const habitBlockReason = await habitToolBatchBlockReason(habitSession, result.toolCalls, augmentedToolContext?.delegatedOrigins, activeToolNames, augmentedToolContext?.noSend === true);
1228
1421
  if (habitBlockReason) {
1229
1422
  streamCallbackBuffer?.discard();
1230
1423
  recordBlockedHabitSurfaceAttempts(habitSession, result.toolCalls, habitBlockReason);
@@ -1243,7 +1436,60 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1243
1436
  });
1244
1437
  continue;
1245
1438
  }
1246
- await streamCallbackBuffer?.flush();
1439
+ const soleTerminalCall = result.toolCalls.length === 1
1440
+ ? result.toolCalls[0]
1441
+ : null;
1442
+ const soleTerminalProjection = soleTerminalCall
1443
+ ? (0, tools_1.resolveToolDefinition)(soleTerminalCall.name)?.terminalProjection
1444
+ : undefined;
1445
+ if (soleTerminalCall && soleTerminalProjection?.mode === "verbatim") {
1446
+ let terminalArgs = {};
1447
+ try {
1448
+ const parsed = JSON.parse(soleTerminalCall.arguments);
1449
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1450
+ terminalArgs = parsed;
1451
+ }
1452
+ }
1453
+ catch {
1454
+ // The registered handler owns exact argument validation.
1455
+ }
1456
+ if (soleTerminalProjection.clearBufferedText
1457
+ || callbacks.settleOutputMode === "final_only") {
1458
+ streamCallbackBuffer?.discard();
1459
+ }
1460
+ else {
1461
+ await streamCallbackBuffer?.flush();
1462
+ }
1463
+ if (soleTerminalProjection.clearBufferedText)
1464
+ callbacks.onClearText?.();
1465
+ callbacks.onToolStart(soleTerminalCall.name, terminalArgs);
1466
+ let terminalResult;
1467
+ try {
1468
+ const execToolFn = options?.execTool ?? tools_1.execTool;
1469
+ terminalResult = await execToolFn(soleTerminalCall.name, terminalArgs, augmentedToolContext);
1470
+ }
1471
+ catch (error) {
1472
+ callbacks.onToolEnd(soleTerminalCall.name, (0, tools_1.summarizeArgs)(soleTerminalCall.name, terminalArgs), false);
1473
+ messages.push(msg);
1474
+ const failure = error instanceof Error ? `error: ${error.message}` : `error: ${String(error)}`;
1475
+ messages.push({ role: "tool", tool_call_id: soleTerminalCall.id, content: failure });
1476
+ providerRuntime.appendToolOutput(soleTerminalCall.id, failure);
1477
+ callbacks.onTextChunk(failure);
1478
+ completion = { answer: failure, intent: "blocked" };
1479
+ outcome = "blocked";
1480
+ done = true;
1481
+ continue;
1482
+ }
1483
+ 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 });
1486
+ providerRuntime.appendToolOutput(soleTerminalCall.id, terminalResult);
1487
+ callbacks.onTextChunk(terminalResult);
1488
+ completion = { answer: terminalResult, intent: "complete" };
1489
+ outcome = "settled";
1490
+ done = true;
1491
+ continue;
1492
+ }
1247
1493
  // Check for settle sole call: intercept before tool execution
1248
1494
  if (isSoleSettle) {
1249
1495
  /* v8 ignore next -- defensive: JSON.parse catch for malformed settle args @preserve */
@@ -1255,8 +1501,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1255
1501
  } })();
1256
1502
  callbacks.onToolStart("settle", settleArgs);
1257
1503
  // Private-runtime attention queue gate: reject settle if items remain
1258
- const attentionQueue = (augmentedToolContext ?? options?.toolContext)?.delegatedOrigins;
1504
+ const attentionQueue = augmentedToolContext?.delegatedOrigins;
1259
1505
  if (isPrivateRuntimeChannel && attentionQueue && attentionQueue.length > 0) {
1506
+ streamCallbackBuffer?.discard();
1260
1507
  callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
1261
1508
  callbacks.onClearText?.();
1262
1509
  messages.push(msg);
@@ -1270,6 +1517,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1270
1517
  const { answer, intent } = parseSettlePayload(result.toolCalls[0].arguments);
1271
1518
  // Private-runtime settle: no CompletionMetadata, "(settled)" ack
1272
1519
  if (isPrivateRuntimeChannel) {
1520
+ streamCallbackBuffer?.discard();
1273
1521
  callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), true);
1274
1522
  messages.push(msg);
1275
1523
  const settled = "(settled)";
@@ -1279,6 +1527,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1279
1527
  done = true;
1280
1528
  continue;
1281
1529
  }
1530
+ // The provider finalizer has already established a top-level string
1531
+ // answer before ordinary settle handling reaches this point.
1282
1532
  const deliveredAnswer = answer;
1283
1533
  const retryError = privateReturnAckLeakError(deliveredAnswer, privateReturnHeldTokens)
1284
1534
  ?? privateReturnMissingPonderError({
@@ -1288,28 +1538,27 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1288
1538
  })
1289
1539
  ?? getSettleRetryError(mustResolveBeforeHandoffActive, intent, sawSteeringFollowUp, options?.delegationDecision, sawSendMessageSelf, sawPonder, sawQuerySession, options?.currentObligation ?? null, options?.activeWorkFrame?.inner?.job, sawExternalStateQuery);
1290
1540
  const validDirectReply = mustResolveBeforeHandoffActive && intent === "direct_reply" && sawSteeringFollowUp;
1291
- const validTerminalIntent = intent === "complete" || intent === "blocked";
1292
- const validClosure = deliveredAnswer != null
1293
- && !retryError
1294
- && (!mustResolveBeforeHandoffActive || validDirectReply || validTerminalIntent);
1295
- if (validClosure) {
1541
+ if (retryError === null) {
1542
+ try {
1543
+ if (!result.settleStreamed) {
1544
+ const acceptedOutputCallbacks = streamCallbackBuffer?.callbacks ?? callbacks;
1545
+ acceptedOutputCallbacks.onTextChunk(deliveredAnswer);
1546
+ }
1547
+ await streamCallbackBuffer?.flush();
1548
+ }
1549
+ catch (error) {
1550
+ callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
1551
+ streamCallbackBuffer?.discard();
1552
+ finishTerminalProviderError(new streaming_1.SettleFinalizationCallbackError(error), "unknown");
1553
+ continue;
1554
+ }
1296
1555
  callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), true);
1297
1556
  completion = {
1298
1557
  answer: deliveredAnswer,
1299
1558
  intent: validDirectReply ? "direct_reply" : intent === "blocked" ? "blocked" : "complete",
1300
1559
  };
1301
- if (result.settleStreamed) {
1302
- // The streaming layer already parsed and emitted the answer
1303
- // progressively via SettleParser. Skip clearing and
1304
- // re-emitting to avoid double-delivery.
1305
- }
1306
- else {
1307
- // Clear any streamed noise (e.g. refusal text) before emitting.
1308
- callbacks.onClearText?.();
1309
- // Emit the answer through the callback pipeline so channels receive it.
1310
- // Never truncate -- channel adapters handle splitting long messages.
1311
- callbacks.onTextChunk(deliveredAnswer);
1312
- }
1560
+ // Retractable owners already hold the validated answer. Final-only
1561
+ // owners receive it here, after every semantic continuation gate.
1313
1562
  messages.push(msg);
1314
1563
  if (validDirectReply) {
1315
1564
  const resumeWork = "direct reply delivered. resume the unresolved obligation now and keep working until you can finish or clearly report that you are blocked.";
@@ -1325,14 +1574,13 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1325
1574
  }
1326
1575
  }
1327
1576
  else {
1328
- // Answer is undefined -- the model's settle was incomplete or
1329
- // malformed. Clear any partial streamed text or noise, then push the
1330
- // assistant msg + error tool result and let the model try again.
1577
+ // The payload is structurally final, but a semantic continuation
1578
+ // gate rejected it. Return that exact gate reason to the model.
1579
+ streamCallbackBuffer?.discard();
1331
1580
  callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
1332
1581
  callbacks.onClearText?.();
1333
1582
  messages.push(msg);
1334
- const toolRetryMessage = retryError
1335
- ?? "your settle was incomplete or malformed. call settle again with your complete response.";
1583
+ const toolRetryMessage = retryError;
1336
1584
  messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: toolRetryMessage });
1337
1585
  providerRuntime.appendToolOutput(result.toolCalls[0].id, toolRetryMessage);
1338
1586
  }
@@ -1341,6 +1589,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1341
1589
  // Check for observe sole call: intercept before tool execution
1342
1590
  const isSoleObserve = result.toolCalls.length === 1 && result.toolCalls[0].name === "observe";
1343
1591
  if (isSoleObserve) {
1592
+ streamCallbackBuffer?.discard();
1344
1593
  /* v8 ignore next -- defensive: JSON.parse catch for malformed observe args @preserve */
1345
1594
  const observeArgs = (() => { try {
1346
1595
  return JSON.parse(result.toolCalls[0].arguments);
@@ -1370,6 +1619,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1370
1619
  // Check for rest sole call: intercept before tool execution
1371
1620
  const isSoleRest = result.toolCalls.length === 1 && result.toolCalls[0].name === "rest";
1372
1621
  if (isSoleRest) {
1622
+ streamCallbackBuffer?.discard();
1373
1623
  const restArgs = (() => { try {
1374
1624
  return JSON.parse(result.toolCalls[0].arguments);
1375
1625
  }
@@ -1378,7 +1628,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1378
1628
  } })();
1379
1629
  callbacks.onToolStart("rest", restArgs);
1380
1630
  // Attention queue gate: reject rest if items remain
1381
- const attentionQueue = (augmentedToolContext ?? options?.toolContext)?.delegatedOrigins;
1631
+ const attentionQueue = augmentedToolContext?.delegatedOrigins;
1382
1632
  if (attentionQueue && attentionQueue.length > 0) {
1383
1633
  callbacks.onToolEnd("rest", (0, tools_1.summarizeArgs)("rest", restArgs), false);
1384
1634
  messages.push(msg);
@@ -1418,13 +1668,29 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1418
1668
  done = true;
1419
1669
  continue;
1420
1670
  }
1671
+ const containsSoleCallOnlyViolation = result.toolCalls.length > 1
1672
+ && result.toolCalls.some((call) => {
1673
+ const terminalProjection = (0, tools_1.resolveToolDefinition)(call.name)?.terminalProjection;
1674
+ return SOLE_CALL_REJECTION[call.name] !== undefined
1675
+ || terminalProjection?.requiresSoleCall === true;
1676
+ });
1677
+ if (callbacks.settleOutputMode === "final_only" && containsSoleCallOnlyViolation) {
1678
+ streamCallbackBuffer?.discard();
1679
+ }
1680
+ else {
1681
+ await streamCallbackBuffer?.flush();
1682
+ }
1421
1683
  messages.push(msg);
1422
1684
  // Execute tools (sole-call tools in mixed calls are rejected inline)
1423
1685
  for (const tc of result.toolCalls) {
1424
1686
  if (signal?.aborted)
1425
1687
  break;
1426
1688
  // Reject sole-call tools when mixed with other tool calls
1427
- const soleCallRejection = SOLE_CALL_REJECTION[tc.name];
1689
+ const terminalProjection = (0, tools_1.resolveToolDefinition)(tc.name)?.terminalProjection;
1690
+ const soleCallRejection = SOLE_CALL_REJECTION[tc.name]
1691
+ ?? (terminalProjection?.requiresSoleCall
1692
+ ? `rejected: ${tc.name} must be the only tool call.`
1693
+ : undefined);
1428
1694
  if (soleCallRejection) {
1429
1695
  messages.push({ role: "tool", tool_call_id: tc.id, content: soleCallRejection });
1430
1696
  providerRuntime.appendToolOutput(tc.id, soleCallRejection);
@@ -1515,13 +1781,13 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1515
1781
  let success = false;
1516
1782
  try {
1517
1783
  const action = parsedArgs.action ?? "create";
1518
- const currentSession = (augmentedToolContext ?? options?.toolContext)?.currentSession;
1784
+ const currentSession = augmentedToolContext?.currentSession;
1519
1785
  const currentOrigin = currentSession
1520
1786
  ? { friendId: currentSession.friendId, channel: currentSession.channel, key: currentSession.key }
1521
1787
  : undefined;
1522
1788
  const isInnerChannel = currentOrigin?.friendId === "self" && currentOrigin?.channel === "inner";
1523
1789
  const shouldCreateReturnObligation = !!currentOrigin && !isInnerChannel;
1524
- const attentionQueue = (augmentedToolContext ?? options?.toolContext)?.delegatedOrigins ?? [];
1790
+ const attentionQueue = augmentedToolContext?.delegatedOrigins ?? [];
1525
1791
  const successCriteria = parseSuccessCriteria(parsedArgs.success_criteria);
1526
1792
  const payload = parsePacketPayload(parsedArgs.payload_json);
1527
1793
  let packet;
@@ -1690,7 +1956,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1690
1956
  let success;
1691
1957
  try {
1692
1958
  const execToolFn = options?.execTool ?? tools_1.execTool;
1693
- toolResult = await execToolFn(tc.name, args, augmentedToolContext ?? options?.toolContext);
1959
+ toolResult = await execToolFn(tc.name, args, augmentedToolContext);
1694
1960
  success = true;
1695
1961
  }
1696
1962
  catch (e) {