@ouro.bot/cli 0.1.0-alpha.804 → 0.1.0-alpha.806
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.json +14 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/core.js +14 -11
- package/dist/heart/daemon/cli-defaults.js +22 -4
- package/dist/heart/daemon/cli-exec.js +95 -6
- package/dist/heart/daemon/cli-help.js +6 -0
- package/dist/heart/daemon/cli-parse.js +52 -0
- package/dist/heart/daemon/daemon-bootstrap-startup.js +8 -0
- package/dist/heart/daemon/daemon-entry.js +2 -1
- package/dist/heart/daemon/daemon.js +94 -47
- package/dist/heart/daemon/sense-manager.js +1 -1
- package/dist/heart/frontend-approval-runtime.js +432 -0
- package/dist/heart/frontend-journal.js +215 -0
- package/dist/heart/frontend-session-service.js +237 -0
- package/dist/heart/frontend-socket-client.js +154 -0
- package/dist/heart/frontend-socket.js +400 -0
- package/dist/heart/mail-import-discovery.js +3 -0
- package/dist/heart/turn-context.js +1 -0
- package/dist/heart/turn-execution-lease.js +30 -0
- package/dist/mind/prompt.js +4 -1
- package/dist/repertoire/mcp-manager.js +82 -17
- package/dist/senses/acp-server.js +386 -0
- package/dist/senses/pipeline.js +5 -0
- package/dist/senses/sanctuary-install-state-contract.js +126 -0
- package/dist/senses/shared-turn.js +369 -257
- package/dist/senses/telegram.js +5 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -60,11 +60,16 @@ const pipeline_1 = require("./pipeline");
|
|
|
60
60
|
const mcp_manager_1 = require("../repertoire/mcp-manager");
|
|
61
61
|
const runtime_1 = require("../nerves/runtime");
|
|
62
62
|
const session_transaction_1 = require("../mind/session-transaction");
|
|
63
|
+
const turn_execution_lease_1 = require("../heart/turn-execution-lease");
|
|
63
64
|
const RESPONSE_CAP = 50_000;
|
|
64
65
|
const OUTWARD_DELIVERY_TOOL_ACKS = new Map([
|
|
65
66
|
["settle", "(delivered)"],
|
|
66
67
|
["speak", "(spoken)"],
|
|
67
68
|
]);
|
|
69
|
+
async function releaseRuntimeMcpServersAfterTurn() {
|
|
70
|
+
const manager = await Promise.resolve().then(() => __importStar(require("../repertoire/mcp-manager")));
|
|
71
|
+
await manager.releaseRuntimeMcpServers();
|
|
72
|
+
}
|
|
68
73
|
/**
|
|
69
74
|
* Strip MiniMax-style `<think>...</think>` reasoning blocks from a response
|
|
70
75
|
* string. Handles unclosed open tags (treats everything from `<think>` to
|
|
@@ -315,6 +320,19 @@ function getSenseSessionPath(agentName, friendId, channel, sessionKey, agentRoot
|
|
|
315
320
|
* this function handles all pipeline wiring.
|
|
316
321
|
*/
|
|
317
322
|
async function runSenseTurn(options) {
|
|
323
|
+
return (0, turn_execution_lease_1.withTurnExecutionLease)(async () => {
|
|
324
|
+
(0, identity_1.setAgentName)(options.agentName);
|
|
325
|
+
try {
|
|
326
|
+
return await runSenseTurnExclusive(options);
|
|
327
|
+
}
|
|
328
|
+
finally {
|
|
329
|
+
if (options.runtimeMcpServers && !options.disableTools) {
|
|
330
|
+
await releaseRuntimeMcpServersAfterTurn();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
async function runSenseTurnExclusive(options) {
|
|
318
336
|
const { agentName, channel, sessionKey, friendId, userMessage } = options;
|
|
319
337
|
(0, runtime_1.emitNervesEvent)({
|
|
320
338
|
component: "senses",
|
|
@@ -364,279 +382,373 @@ async function runSenseTurn(options) {
|
|
|
364
382
|
const resolver = new friends_1.FriendResolver(friendStore, resolverParams);
|
|
365
383
|
// Initialize MCP manager so MCP tools appear as first-class tools in the agent's tool list.
|
|
366
384
|
// Runtime MCP servers (e.g. Workbench's ouro_workbench) are passed per-turn for THIS agent only.
|
|
367
|
-
const mcpManager =
|
|
385
|
+
const mcpManager = options.disableTools
|
|
386
|
+
? undefined
|
|
387
|
+
: await (0, mcp_manager_1.getSharedMcpManager)(options.runtimeMcpServers ? { runtimeServers: options.runtimeMcpServers } : undefined) ?? undefined;
|
|
368
388
|
// Session path and loading
|
|
369
|
-
const
|
|
389
|
+
const ephemeralRoot = options.disablePersistence
|
|
390
|
+
? fs.mkdtempSync(path.join(os.tmpdir(), "ouro-observe-only-"))
|
|
391
|
+
: null;
|
|
392
|
+
const sessionDir = ephemeralRoot ?? path.join(agentRoot, "state", "sessions", friendId, channel);
|
|
370
393
|
fs.mkdirSync(sessionDir, { recursive: true });
|
|
371
|
-
const sessPath =
|
|
394
|
+
const sessPath = ephemeralRoot
|
|
395
|
+
? path.join(ephemeralRoot, "session.json")
|
|
396
|
+
: getSenseSessionPath(agentName, friendId, channel, sessionKey, agentRoot);
|
|
397
|
+
const reportedSessionPath = ephemeralRoot ? undefined : sessPath;
|
|
372
398
|
const runWithLease = options._withSessionTurnLease ?? session_transaction_1.withSessionTurnLease;
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
399
|
+
try {
|
|
400
|
+
return await runWithLease(sessPath, async (sessionTurnLease) => {
|
|
401
|
+
const baseSessionRevision = (0, session_transaction_1.readSessionTransaction)(sessPath, sessionTurnLease).revision;
|
|
402
|
+
const existing = options.disablePersistence ? undefined : (0, context_1.loadSession)(sessPath);
|
|
403
|
+
const precommittedIngressEvent = options.precommittedIngress
|
|
404
|
+
? existing?.events?.find((candidate) => candidate.id === options.precommittedIngress.eventId)
|
|
405
|
+
: undefined;
|
|
406
|
+
if (options.precommittedIngress) {
|
|
407
|
+
const latestUserEvent = existing?.events?.filter((candidate) => candidate.role === "user").at(-1);
|
|
408
|
+
if (!precommittedIngressEvent || precommittedIngressEvent !== latestUserEvent || precommittedIngressEvent.role !== "user" || precommittedIngressEvent.content !== userMessage
|
|
409
|
+
|| !precommittedIngressEvent.relations.references.includes(options.precommittedIngress.reference)) {
|
|
410
|
+
throw new Error("shared turn precommitted ingress is missing, mismatched, or no longer current");
|
|
411
|
+
}
|
|
384
412
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
413
|
+
const existingEventIds = new Set(existing?.events?.map((event) => event.id) ?? []);
|
|
414
|
+
const existingStructuredOutputIds = new Set(existing?.structuredOutputs?.map((output) => output.id) ?? []);
|
|
415
|
+
let sessionState = existing?.state;
|
|
416
|
+
let persistPromise;
|
|
417
|
+
const sessionMessages = existing?.messages && existing.messages.length > 0
|
|
418
|
+
? existing.messages
|
|
419
|
+
: [{
|
|
420
|
+
role: "system",
|
|
421
|
+
content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)(channel, options.disableTools ? { tools: [], hardDisableTools: true } : {}, undefined)),
|
|
422
|
+
}];
|
|
423
|
+
if (precommittedIngressEvent) {
|
|
424
|
+
const projectedIngress = exactProjectedIngressMessage(existing, sessionMessages, precommittedIngressEvent.id);
|
|
425
|
+
if (!projectedIngress || projectedIngress.role !== "user" || projectedIngress.content !== userMessage)
|
|
426
|
+
throw new Error("shared turn precommitted ingress is absent from the provider projection");
|
|
427
|
+
(0, session_events_1.stampIngressRelations)(projectedIngress, {
|
|
428
|
+
replyToEventId: precommittedIngressEvent.relations.replyToEventId,
|
|
429
|
+
threadRootEventId: precommittedIngressEvent.relations.threadRootEventId,
|
|
430
|
+
references: precommittedIngressEvent.relations.references,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
// Pending dir
|
|
434
|
+
const pendingDir = (0, pending_1.getPendingDir)(agentName, friendId, channel, sessionKey);
|
|
435
|
+
// Accumulate outward text through the same callback boundary used by chat
|
|
436
|
+
// channels. `speak` flushes pending text immediately; `settle` is delivered
|
|
437
|
+
// once the turn completes.
|
|
438
|
+
let committedResponseText = "";
|
|
439
|
+
let pendingResponseText = "";
|
|
440
|
+
let terminalDeliveryKind = "text";
|
|
441
|
+
const deliveries = [];
|
|
442
|
+
const deliveryFailures = [];
|
|
443
|
+
const deliveryAttempts = [];
|
|
444
|
+
let providerInvocationCount = 0;
|
|
445
|
+
let toolInvocationCount = 0;
|
|
446
|
+
let hadReasoningChunk = false;
|
|
447
|
+
const commitResponseText = (text) => {
|
|
448
|
+
const cleaned = stripThinkBlocks(text);
|
|
449
|
+
/* v8 ignore next -- deliverPending strips first; this is a defensive direct-call guard @preserve */
|
|
450
|
+
if (!cleaned)
|
|
451
|
+
return;
|
|
452
|
+
committedResponseText = committedResponseText
|
|
453
|
+
? `${committedResponseText}\n${cleaned}`
|
|
454
|
+
: cleaned;
|
|
455
|
+
};
|
|
456
|
+
const deliveryErrorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
457
|
+
const emitFrontendEvent = (event) => {
|
|
458
|
+
options.frontendEventSink?.onEvent(event);
|
|
459
|
+
};
|
|
460
|
+
const deliverPending = async (kind, optionsForDelivery) => {
|
|
461
|
+
const text = stripThinkBlocks(pendingResponseText);
|
|
462
|
+
pendingResponseText = "";
|
|
463
|
+
if (!text)
|
|
464
|
+
return undefined;
|
|
465
|
+
const delivery = { kind, text };
|
|
466
|
+
const attempt = { kind, text, delivered: false };
|
|
467
|
+
const attemptIndex = deliveryAttempts.length;
|
|
468
|
+
deliveryAttempts.push(attempt);
|
|
469
|
+
try {
|
|
470
|
+
await options.deliverySink?.onDelivery(delivery);
|
|
471
|
+
attempt.delivered = true;
|
|
472
|
+
deliveries.push(delivery);
|
|
473
|
+
commitResponseText(text);
|
|
474
|
+
emitFrontendEvent({ type: "assistant_delivery", data: { kind, text } });
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
const failure = { ...delivery, error: deliveryErrorMessage(error) };
|
|
478
|
+
deliveryFailures.push(failure);
|
|
479
|
+
(0, runtime_1.emitNervesEvent)({
|
|
480
|
+
level: "error",
|
|
481
|
+
component: "senses",
|
|
482
|
+
event: "senses.shared_turn_delivery_error",
|
|
483
|
+
message: "shared turn outward delivery failed",
|
|
484
|
+
meta: { agentName, channel, sessionKey, friendId, kind, error: failure.error, textLength: text.length },
|
|
485
|
+
});
|
|
486
|
+
if (optionsForDelivery.throwOnError)
|
|
487
|
+
throw error;
|
|
488
|
+
commitResponseText(text);
|
|
489
|
+
}
|
|
490
|
+
return attemptIndex;
|
|
491
|
+
};
|
|
492
|
+
/* v8 ignore start — callback stubs are exercised through the pipeline integration */
|
|
493
|
+
const callbacks = {
|
|
494
|
+
settleOutputMode: "retractable_buffer",
|
|
495
|
+
onModelStart: () => {
|
|
496
|
+
providerInvocationCount += 1;
|
|
497
|
+
if (options.turnMetricsObserver)
|
|
498
|
+
options.turnMetricsObserver.providerInvocationCount += 1;
|
|
499
|
+
emitFrontendEvent({ type: "model_started", data: {} });
|
|
500
|
+
},
|
|
501
|
+
onModelStreamStart: () => emitFrontendEvent({ type: "model_stream_started", data: {} }),
|
|
502
|
+
onTextChunk: (chunk) => {
|
|
503
|
+
pendingResponseText += chunk;
|
|
504
|
+
emitFrontendEvent({ type: "text_delta", data: { text: chunk } });
|
|
505
|
+
},
|
|
506
|
+
onReasoningChunk: (chunk) => {
|
|
507
|
+
hadReasoningChunk = true;
|
|
508
|
+
emitFrontendEvent({ type: "reasoning_delta", data: { text: chunk } });
|
|
509
|
+
},
|
|
510
|
+
onToolStart: (name, args) => {
|
|
511
|
+
toolInvocationCount += 1;
|
|
512
|
+
if (options.turnMetricsObserver)
|
|
513
|
+
options.turnMetricsObserver.toolInvocationCount += 1;
|
|
514
|
+
emitFrontendEvent({ type: "tool_started", data: { name, args } });
|
|
515
|
+
},
|
|
516
|
+
onToolEnd: (name, _summary, success) => {
|
|
517
|
+
if (name === "settle" && success)
|
|
518
|
+
terminalDeliveryKind = "settle";
|
|
519
|
+
emitFrontendEvent({ type: "tool_completed", data: { name, summary: _summary, success } });
|
|
520
|
+
},
|
|
521
|
+
onError: (error, severity) => {
|
|
522
|
+
emitFrontendEvent({ type: "error", data: { message: error.message, severity } });
|
|
523
|
+
},
|
|
524
|
+
onClearText: () => {
|
|
525
|
+
pendingResponseText = "";
|
|
526
|
+
emitFrontendEvent({ type: "text_cleared", data: {} });
|
|
527
|
+
},
|
|
528
|
+
flushNow: async () => { await deliverPending("speak", { throwOnError: true }); },
|
|
529
|
+
};
|
|
530
|
+
/* v8 ignore stop */
|
|
531
|
+
// Run the pipeline
|
|
532
|
+
const inboundMessages = [];
|
|
533
|
+
if (!options.precommittedIngress) {
|
|
534
|
+
const userMsg = { role: "user", content: userMessage };
|
|
535
|
+
(0, session_events_1.stampIngressTime)(userMsg);
|
|
536
|
+
if (options.ingressRelations)
|
|
537
|
+
(0, session_events_1.stampIngressRelations)(userMsg, options.ingressRelations);
|
|
538
|
+
inboundMessages.push(userMsg);
|
|
539
|
+
}
|
|
540
|
+
const turnResult = await (0, pipeline_1.handleInboundTurn)({
|
|
541
|
+
channel,
|
|
542
|
+
latencyMode: options.latencyMode,
|
|
543
|
+
sessionKey,
|
|
544
|
+
capabilities,
|
|
545
|
+
messages: inboundMessages,
|
|
546
|
+
callbacks,
|
|
547
|
+
sessionTurnLease,
|
|
548
|
+
/* v8 ignore start — delegation wrappers; pipeline integration tested separately */
|
|
549
|
+
friendResolver: { resolve: () => resolver.resolve() },
|
|
550
|
+
sessionLoader: {
|
|
551
|
+
loadOrCreate: () => Promise.resolve({
|
|
552
|
+
messages: sessionMessages,
|
|
553
|
+
sessionPath: sessPath,
|
|
554
|
+
state: sessionState,
|
|
555
|
+
events: existing?.events,
|
|
556
|
+
structuredOutputs: existing?.structuredOutputs,
|
|
557
|
+
}),
|
|
558
|
+
},
|
|
559
|
+
/* v8 ignore stop */
|
|
560
|
+
pendingDir,
|
|
561
|
+
friendStore,
|
|
562
|
+
signal: options.signal,
|
|
563
|
+
provider: resolverParams.provider,
|
|
564
|
+
externalId: resolverParams.externalId,
|
|
565
|
+
tenantId: resolverParams.tenantId,
|
|
566
|
+
enforceTrustGate: trust_gate_1.enforceTrustGate,
|
|
567
|
+
drainPending: options.disablePersistence ? () => [] : pending_1.drainPending,
|
|
568
|
+
runAgentOptions: {
|
|
569
|
+
mcpManager,
|
|
570
|
+
...(options.disableTools ? { tools: [], hardDisableTools: true } : {}),
|
|
571
|
+
...(options.approvalCoordinatorFactory && !options.disablePersistence ? { approvalCoordinator: options.approvalCoordinatorFactory({ sessionPath: sessPath, baseSessionRevision }) } : {}),
|
|
572
|
+
...(options.latencyMode === "live" ? { skipKeptNotes: true } : {}),
|
|
573
|
+
...(options.orientationFrame ? { orientationFrame: options.orientationFrame } : {}),
|
|
574
|
+
toolContext: {
|
|
575
|
+
signin: async () => undefined,
|
|
576
|
+
...(options.toolContext ? options.toolContext : {}),
|
|
577
|
+
currentUserMessage: userMessage,
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
...(options.prepareRunAgentOptions ? { prepareRunAgentOptions: options.prepareRunAgentOptions } : {}),
|
|
581
|
+
/* v8 ignore start — delegation wrappers; these just forward to the real functions */
|
|
582
|
+
runAgent: (msgs, cb, ch, sig, opts) => (0, core_1.runAgent)(msgs, cb, ch, sig, opts),
|
|
583
|
+
postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
|
|
584
|
+
const prepared = (0, context_2.postTurnTrim)(turnMessages, usage, hooks);
|
|
585
|
+
sessionState = state;
|
|
586
|
+
if (options.disablePersistence)
|
|
587
|
+
return;
|
|
588
|
+
persistPromise = (0, context_2.deferPostTurnPersist)(sessionPathArg, prepared, usage, state);
|
|
589
|
+
},
|
|
590
|
+
/* v8 ignore stop */
|
|
591
|
+
accumulateFriendTokens: options.disablePersistence ? async () => undefined : friends_1.accumulateFriendTokens,
|
|
400
592
|
});
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
return
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
593
|
+
if (turnResult.gateResult && !turnResult.gateResult.allowed) {
|
|
594
|
+
const blockedResponse = "autoReply" in turnResult.gateResult
|
|
595
|
+
? turnResult.gateResult.autoReply
|
|
596
|
+
: `(blocked by trust gate: ${turnResult.gateResult.reason})`;
|
|
597
|
+
return {
|
|
598
|
+
response: blockedResponse,
|
|
599
|
+
ponderDeferred: false,
|
|
600
|
+
deliveries,
|
|
601
|
+
deliveryFailures,
|
|
602
|
+
providerInvocationCount,
|
|
603
|
+
toolInvocationCount,
|
|
604
|
+
sessionPath: reportedSessionPath,
|
|
605
|
+
turnOutcome: turnResult.turnOutcome ?? "blocked",
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
if (turnResult.turnOutcome === "aborted") {
|
|
609
|
+
pendingResponseText = "";
|
|
610
|
+
if (persistPromise)
|
|
611
|
+
await persistPromise;
|
|
612
|
+
return {
|
|
613
|
+
response: "",
|
|
614
|
+
ponderDeferred: false,
|
|
615
|
+
deliveries,
|
|
616
|
+
deliveryFailures,
|
|
617
|
+
providerInvocationCount,
|
|
618
|
+
toolInvocationCount,
|
|
619
|
+
sessionPath: reportedSessionPath,
|
|
620
|
+
turnOutcome: "aborted",
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
if (turnResult.turnOutcome === "suspended") {
|
|
624
|
+
if (!turnResult.suspension)
|
|
625
|
+
throw new Error("suspended shared turn omitted durable approval suspension");
|
|
626
|
+
pendingResponseText = "";
|
|
627
|
+
return {
|
|
628
|
+
response: "",
|
|
629
|
+
ponderDeferred: false,
|
|
630
|
+
deliveries,
|
|
631
|
+
deliveryFailures,
|
|
632
|
+
providerInvocationCount,
|
|
633
|
+
toolInvocationCount,
|
|
634
|
+
sessionPath: reportedSessionPath,
|
|
635
|
+
turnOutcome: "suspended",
|
|
636
|
+
suspension: turnResult.suspension,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
const persistedEvents = persistPromise ? await persistPromise : [];
|
|
640
|
+
const terminalEvents = persistedEvents.length > 0
|
|
641
|
+
? persistedEvents
|
|
642
|
+
: rawSessionEvents((0, session_transaction_1.readSessionTransaction)(sessPath, sessionTurnLease).value);
|
|
643
|
+
const eventView = currentTurnEventView(terminalEvents, existingEventIds, userMessage, options.precommittedIngress, options.ingressRelations);
|
|
644
|
+
let finalDeliveryKind = terminalDeliveryKind;
|
|
645
|
+
const acceptedTerminalOutcome = turnResult.turnOutcome === "settled" || turnResult.turnOutcome === "blocked";
|
|
646
|
+
const failoverText = turnResult.turnOutcome === "errored" ? turnResult.failoverMessage?.trim() : undefined;
|
|
647
|
+
const expectsOutwardResponse = acceptedTerminalOutcome || turnResult.turnOutcome === "command" || Boolean(failoverText);
|
|
648
|
+
let finalCausalCoordinate;
|
|
649
|
+
if (acceptedTerminalOutcome) {
|
|
650
|
+
const completionText = stripThinkBlocks(turnResult.completion?.answer ?? "");
|
|
651
|
+
let authoritativeText = completionText;
|
|
652
|
+
if (!authoritativeText && eventView?.terminal) {
|
|
653
|
+
authoritativeText = eventView.terminal.text;
|
|
654
|
+
finalDeliveryKind = eventView.terminal.kind;
|
|
655
|
+
}
|
|
656
|
+
if (eventView?.terminal?.kind === finalDeliveryKind && eventView.terminal.text === authoritativeText)
|
|
657
|
+
finalCausalCoordinate = eventView.terminal;
|
|
658
|
+
const terminalAlreadyDelivered = finalCausalCoordinate
|
|
659
|
+
? alignedDeliveryCoordinates(eventView, deliveryAttempts).includes(finalCausalCoordinate)
|
|
660
|
+
: false;
|
|
661
|
+
pendingResponseText = authoritativeText && !terminalAlreadyDelivered ? authoritativeText : "";
|
|
662
|
+
}
|
|
663
|
+
else if (turnResult.turnOutcome === "command") {
|
|
664
|
+
// Slash-command text is emitted directly by the pipeline and has no assistant event.
|
|
665
|
+
}
|
|
666
|
+
else if (failoverText) {
|
|
667
|
+
pendingResponseText = failoverText;
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
pendingResponseText = "";
|
|
671
|
+
}
|
|
672
|
+
if (persistPromise && options.frontendEventSink && !options.disablePersistence) {
|
|
673
|
+
const currentStructuredOutputs = (0, context_1.loadSession)(sessPath)?.structuredOutputs ?? [];
|
|
674
|
+
for (const output of currentStructuredOutputs) {
|
|
675
|
+
if (!existingStructuredOutputIds.has(output.id)) {
|
|
676
|
+
emitFrontendEvent({ type: "structured_output", data: { output } });
|
|
677
|
+
}
|
|
678
|
+
}
|
|
440
679
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
680
|
+
const finalDeliveryAttemptIndex = await deliverPending(finalDeliveryKind, { throwOnError: false });
|
|
681
|
+
const ponderDeferred = false;
|
|
682
|
+
// Build response
|
|
683
|
+
let finalResponse;
|
|
684
|
+
const finalDeliveryAttempt = finalDeliveryAttemptIndex === undefined ? undefined : deliveryAttempts[finalDeliveryAttemptIndex];
|
|
685
|
+
const responseDeliveryFailure = finalDeliveryAttempt && !finalDeliveryAttempt.delivered ? deliveryFailures.at(-1) : undefined;
|
|
686
|
+
const responseCausalSessionEventId = finalDeliveryAttempt && !finalDeliveryAttempt.delivered ? finalCausalCoordinate?.eventId : undefined;
|
|
687
|
+
if (committedResponseText.length === 0) {
|
|
688
|
+
if (!expectsOutwardResponse) {
|
|
689
|
+
finalResponse = "";
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
const emptyFallback = options.emptyResponseFallback?.();
|
|
693
|
+
finalResponse = emptyFallback ?? (hadReasoningChunk ? "" : "(agent responded but response was empty)");
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
finalResponse = committedResponseText;
|
|
698
|
+
}
|
|
699
|
+
// Strip MiniMax-style <think>...</think> blocks from the final response.
|
|
700
|
+
// When a reasoning-style model emits only a think block and no final answer
|
|
701
|
+
// (no settle tool call, no post-think text), the readback path above
|
|
702
|
+
// surfaces the raw saved assistant content — which includes the think tags
|
|
703
|
+
// and renders as empty (or as raw reasoning) on MCP/CLI clients. Strip
|
|
704
|
+
// here so the caller sees the actual delivered text. If only reasoning
|
|
705
|
+
// came through and nothing else, surface a clear diagnostic message
|
|
706
|
+
// instead of a blank response so the operator knows what happened.
|
|
707
|
+
finalResponse = stripThinkBlocks(finalResponse);
|
|
708
|
+
if (finalResponse.length === 0 && expectsOutwardResponse) {
|
|
444
709
|
(0, runtime_1.emitNervesEvent)({
|
|
445
|
-
level: "
|
|
710
|
+
level: "warn",
|
|
446
711
|
component: "senses",
|
|
447
|
-
event: "senses.
|
|
448
|
-
message: "
|
|
449
|
-
meta: { agentName, channel, sessionKey, friendId
|
|
712
|
+
event: "senses.shared_turn_only_reasoning",
|
|
713
|
+
message: "agent produced only <think> reasoning with no final answer — likely a model that closed the think tag without continuing",
|
|
714
|
+
meta: { agentName, channel, sessionKey, friendId },
|
|
450
715
|
});
|
|
451
|
-
|
|
452
|
-
throw error;
|
|
453
|
-
commitResponseText(text);
|
|
716
|
+
finalResponse = "(agent produced reasoning but no final answer this turn — try again, or check the session transcript for the trace)";
|
|
454
717
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
onToolStart: () => { toolInvocationCount += 1; if (options.turnMetricsObserver)
|
|
466
|
-
options.turnMetricsObserver.toolInvocationCount += 1; },
|
|
467
|
-
onToolEnd: (name, _summary, success) => {
|
|
468
|
-
if (name === "settle" && success)
|
|
469
|
-
terminalDeliveryKind = "settle";
|
|
470
|
-
},
|
|
471
|
-
onError: () => { },
|
|
472
|
-
onClearText: () => { pendingResponseText = ""; },
|
|
473
|
-
flushNow: async () => { await deliverPending("speak", { throwOnError: true }); },
|
|
474
|
-
};
|
|
475
|
-
/* v8 ignore stop */
|
|
476
|
-
// Run the pipeline
|
|
477
|
-
const inboundMessages = [];
|
|
478
|
-
if (!options.precommittedIngress) {
|
|
479
|
-
const userMsg = { role: "user", content: userMessage };
|
|
480
|
-
(0, session_events_1.stampIngressTime)(userMsg);
|
|
481
|
-
if (options.ingressRelations)
|
|
482
|
-
(0, session_events_1.stampIngressRelations)(userMsg, options.ingressRelations);
|
|
483
|
-
inboundMessages.push(userMsg);
|
|
484
|
-
}
|
|
485
|
-
const turnResult = await (0, pipeline_1.handleInboundTurn)({
|
|
486
|
-
channel,
|
|
487
|
-
latencyMode: options.latencyMode,
|
|
488
|
-
sessionKey,
|
|
489
|
-
capabilities,
|
|
490
|
-
messages: inboundMessages,
|
|
491
|
-
callbacks,
|
|
492
|
-
sessionTurnLease,
|
|
493
|
-
/* v8 ignore start — delegation wrappers; pipeline integration tested separately */
|
|
494
|
-
friendResolver: { resolve: () => resolver.resolve() },
|
|
495
|
-
sessionLoader: {
|
|
496
|
-
loadOrCreate: () => Promise.resolve({
|
|
497
|
-
messages: sessionMessages,
|
|
498
|
-
sessionPath: sessPath,
|
|
499
|
-
state: sessionState,
|
|
500
|
-
events: existing?.events,
|
|
501
|
-
structuredOutputs: existing?.structuredOutputs,
|
|
502
|
-
}),
|
|
503
|
-
},
|
|
504
|
-
/* v8 ignore stop */
|
|
505
|
-
pendingDir,
|
|
506
|
-
friendStore,
|
|
507
|
-
provider: resolverParams.provider,
|
|
508
|
-
externalId: resolverParams.externalId,
|
|
509
|
-
tenantId: resolverParams.tenantId,
|
|
510
|
-
enforceTrustGate: trust_gate_1.enforceTrustGate,
|
|
511
|
-
drainPending: pending_1.drainPending,
|
|
512
|
-
runAgentOptions: {
|
|
513
|
-
mcpManager,
|
|
514
|
-
...(options.approvalCoordinatorFactory ? { approvalCoordinator: options.approvalCoordinatorFactory({ sessionPath: sessPath, baseSessionRevision }) } : {}),
|
|
515
|
-
...(options.latencyMode === "live" ? { skipKeptNotes: true } : {}),
|
|
516
|
-
...(options.orientationFrame ? { orientationFrame: options.orientationFrame } : {}),
|
|
517
|
-
toolContext: {
|
|
518
|
-
signin: async () => undefined,
|
|
519
|
-
...(options.toolContext ? options.toolContext : {}),
|
|
520
|
-
currentUserMessage: userMessage,
|
|
521
|
-
},
|
|
522
|
-
},
|
|
523
|
-
...(options.prepareRunAgentOptions ? { prepareRunAgentOptions: options.prepareRunAgentOptions } : {}),
|
|
524
|
-
/* v8 ignore start — delegation wrappers; these just forward to the real functions */
|
|
525
|
-
runAgent: (msgs, cb, ch, sig, opts) => (0, core_1.runAgent)(msgs, cb, ch, sig, opts),
|
|
526
|
-
postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
|
|
527
|
-
const prepared = (0, context_2.postTurnTrim)(turnMessages, usage, hooks);
|
|
528
|
-
sessionState = state;
|
|
529
|
-
persistPromise = (0, context_2.deferPostTurnPersist)(sessionPathArg, prepared, usage, state);
|
|
530
|
-
},
|
|
531
|
-
/* v8 ignore stop */
|
|
532
|
-
accumulateFriendTokens: friends_1.accumulateFriendTokens,
|
|
533
|
-
});
|
|
534
|
-
if (turnResult.gateResult && !turnResult.gateResult.allowed) {
|
|
535
|
-
const blockedResponse = "autoReply" in turnResult.gateResult
|
|
536
|
-
? turnResult.gateResult.autoReply
|
|
537
|
-
: `(blocked by trust gate: ${turnResult.gateResult.reason})`;
|
|
718
|
+
// Cap response length
|
|
719
|
+
if (finalResponse.length > RESPONSE_CAP) {
|
|
720
|
+
finalResponse = finalResponse.slice(0, RESPONSE_CAP) + "\n\n[truncated — response exceeded 50K characters]";
|
|
721
|
+
}
|
|
722
|
+
(0, runtime_1.emitNervesEvent)({
|
|
723
|
+
component: "senses",
|
|
724
|
+
event: "senses.shared_turn_end",
|
|
725
|
+
message: "shared turn runner complete",
|
|
726
|
+
meta: { agentName, channel, sessionKey, friendId, ponderDeferred, responseLength: finalResponse.length },
|
|
727
|
+
});
|
|
538
728
|
return {
|
|
539
|
-
response:
|
|
540
|
-
ponderDeferred
|
|
729
|
+
response: finalResponse,
|
|
730
|
+
ponderDeferred,
|
|
541
731
|
deliveries,
|
|
542
732
|
deliveryFailures,
|
|
733
|
+
...(responseDeliveryFailure ? { responseDeliveryFailure } : {}),
|
|
543
734
|
providerInvocationCount,
|
|
544
735
|
toolInvocationCount,
|
|
545
|
-
sessionPath:
|
|
736
|
+
sessionPath: reportedSessionPath,
|
|
737
|
+
turnOutcome: turnResult.turnOutcome,
|
|
738
|
+
...(deliveries.length > 0 ? { causalSessionEventIds: causalSessionEventIds(eventView, deliveryAttempts, finalDeliveryAttemptIndex, finalCausalCoordinate) } : {}),
|
|
739
|
+
...(responseCausalSessionEventId ? { responseCausalSessionEventId } : {}),
|
|
546
740
|
};
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
const expectsOutwardResponse = acceptedTerminalOutcome || turnResult.turnOutcome === "command" || Boolean(failoverText);
|
|
557
|
-
let finalCausalCoordinate;
|
|
558
|
-
if (acceptedTerminalOutcome) {
|
|
559
|
-
const completionText = stripThinkBlocks(turnResult.completion?.answer ?? "");
|
|
560
|
-
let authoritativeText = completionText;
|
|
561
|
-
if (!authoritativeText && eventView?.terminal) {
|
|
562
|
-
authoritativeText = eventView.terminal.text;
|
|
563
|
-
finalDeliveryKind = eventView.terminal.kind;
|
|
564
|
-
}
|
|
565
|
-
if (eventView?.terminal?.kind === finalDeliveryKind && eventView.terminal.text === authoritativeText)
|
|
566
|
-
finalCausalCoordinate = eventView.terminal;
|
|
567
|
-
const terminalAlreadyDelivered = finalCausalCoordinate
|
|
568
|
-
? alignedDeliveryCoordinates(eventView, deliveryAttempts).includes(finalCausalCoordinate)
|
|
569
|
-
: false;
|
|
570
|
-
pendingResponseText = authoritativeText && !terminalAlreadyDelivered ? authoritativeText : "";
|
|
571
|
-
}
|
|
572
|
-
else if (turnResult.turnOutcome === "command") {
|
|
573
|
-
// Slash-command text is emitted directly by the pipeline and has no assistant event.
|
|
574
|
-
}
|
|
575
|
-
else if (failoverText) {
|
|
576
|
-
pendingResponseText = failoverText;
|
|
577
|
-
}
|
|
578
|
-
else {
|
|
579
|
-
pendingResponseText = "";
|
|
580
|
-
}
|
|
581
|
-
const finalDeliveryAttemptIndex = await deliverPending(finalDeliveryKind, { throwOnError: false });
|
|
582
|
-
const ponderDeferred = false;
|
|
583
|
-
// Build response
|
|
584
|
-
let finalResponse;
|
|
585
|
-
const finalDeliveryAttempt = finalDeliveryAttemptIndex === undefined ? undefined : deliveryAttempts[finalDeliveryAttemptIndex];
|
|
586
|
-
const responseDeliveryFailure = finalDeliveryAttempt && !finalDeliveryAttempt.delivered ? deliveryFailures.at(-1) : undefined;
|
|
587
|
-
const responseCausalSessionEventId = finalDeliveryAttempt && !finalDeliveryAttempt.delivered ? finalCausalCoordinate?.eventId : undefined;
|
|
588
|
-
if (committedResponseText.length === 0) {
|
|
589
|
-
if (!expectsOutwardResponse) {
|
|
590
|
-
finalResponse = "";
|
|
591
|
-
}
|
|
592
|
-
else {
|
|
593
|
-
const emptyFallback = options.emptyResponseFallback?.();
|
|
594
|
-
finalResponse = emptyFallback ?? (hadReasoningChunk ? "" : "(agent responded but response was empty)");
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
finally {
|
|
744
|
+
if (ephemeralRoot) {
|
|
745
|
+
for (const entry of fs.readdirSync(ephemeralRoot, { withFileTypes: true })) {
|
|
746
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) {
|
|
747
|
+
throw new Error(`observe-only session cleanup found unexpected entry: ${entry.name}`);
|
|
748
|
+
}
|
|
749
|
+
fs.unlinkSync(path.join(ephemeralRoot, entry.name));
|
|
595
750
|
}
|
|
751
|
+
fs.rmdirSync(ephemeralRoot);
|
|
596
752
|
}
|
|
597
|
-
|
|
598
|
-
finalResponse = committedResponseText;
|
|
599
|
-
}
|
|
600
|
-
// Strip MiniMax-style <think>...</think> blocks from the final response.
|
|
601
|
-
// When a reasoning-style model emits only a think block and no final answer
|
|
602
|
-
// (no settle tool call, no post-think text), the readback path above
|
|
603
|
-
// surfaces the raw saved assistant content — which includes the think tags
|
|
604
|
-
// and renders as empty (or as raw reasoning) on MCP/CLI clients. Strip
|
|
605
|
-
// here so the caller sees the actual delivered text. If only reasoning
|
|
606
|
-
// came through and nothing else, surface a clear diagnostic message
|
|
607
|
-
// instead of a blank response so the operator knows what happened.
|
|
608
|
-
finalResponse = stripThinkBlocks(finalResponse);
|
|
609
|
-
if (finalResponse.length === 0 && expectsOutwardResponse) {
|
|
610
|
-
(0, runtime_1.emitNervesEvent)({
|
|
611
|
-
level: "warn",
|
|
612
|
-
component: "senses",
|
|
613
|
-
event: "senses.shared_turn_only_reasoning",
|
|
614
|
-
message: "agent produced only <think> reasoning with no final answer — likely a model that closed the think tag without continuing",
|
|
615
|
-
meta: { agentName, channel, sessionKey, friendId },
|
|
616
|
-
});
|
|
617
|
-
finalResponse = "(agent produced reasoning but no final answer this turn — try again, or check the session transcript for the trace)";
|
|
618
|
-
}
|
|
619
|
-
// Cap response length
|
|
620
|
-
if (finalResponse.length > RESPONSE_CAP) {
|
|
621
|
-
finalResponse = finalResponse.slice(0, RESPONSE_CAP) + "\n\n[truncated — response exceeded 50K characters]";
|
|
622
|
-
}
|
|
623
|
-
(0, runtime_1.emitNervesEvent)({
|
|
624
|
-
component: "senses",
|
|
625
|
-
event: "senses.shared_turn_end",
|
|
626
|
-
message: "shared turn runner complete",
|
|
627
|
-
meta: { agentName, channel, sessionKey, friendId, ponderDeferred, responseLength: finalResponse.length },
|
|
628
|
-
});
|
|
629
|
-
return {
|
|
630
|
-
response: finalResponse,
|
|
631
|
-
ponderDeferred,
|
|
632
|
-
deliveries,
|
|
633
|
-
deliveryFailures,
|
|
634
|
-
...(responseDeliveryFailure ? { responseDeliveryFailure } : {}),
|
|
635
|
-
providerInvocationCount,
|
|
636
|
-
toolInvocationCount,
|
|
637
|
-
sessionPath: sessPath,
|
|
638
|
-
...(deliveries.length > 0 ? { causalSessionEventIds: causalSessionEventIds(eventView, deliveryAttempts, finalDeliveryAttemptIndex, finalCausalCoordinate) } : {}),
|
|
639
|
-
...(responseCausalSessionEventId ? { responseCausalSessionEventId } : {}),
|
|
640
|
-
};
|
|
641
|
-
});
|
|
753
|
+
}
|
|
642
754
|
}
|