@elizaos/agent 2.0.0-alpha.155 → 2.0.0-alpha.161

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 (46) hide show
  1. package/package.json +5 -5
  2. package/packages/agent/src/api/plugin-routes.d.ts.map +1 -1
  3. package/packages/agent/src/api/plugin-routes.js +50 -0
  4. package/packages/agent/src/config/types.eliza.d.ts +2 -0
  5. package/packages/agent/src/config/types.eliza.d.ts.map +1 -1
  6. package/packages/typescript/src/features/autonomy/execution-facade.d.ts.map +1 -1
  7. package/packages/typescript/src/features/autonomy/execution-facade.js +27 -9
  8. package/packages/typescript/src/optimization/ab-analysis.d.ts +3 -0
  9. package/packages/typescript/src/optimization/ab-analysis.d.ts.map +1 -0
  10. package/packages/typescript/src/optimization/ab-analysis.js +8 -0
  11. package/packages/typescript/src/optimization-root-dir.d.ts +3 -0
  12. package/packages/typescript/src/optimization-root-dir.d.ts.map +1 -0
  13. package/packages/typescript/src/optimization-root-dir.js +9 -0
  14. package/packages/typescript/src/plugin.d.ts.map +1 -1
  15. package/packages/typescript/src/plugin.js +10 -3
  16. package/packages/typescript/src/runtime.d.ts +41 -3
  17. package/packages/typescript/src/runtime.d.ts.map +1 -1
  18. package/packages/typescript/src/runtime.js +700 -30
  19. package/packages/typescript/src/services/message.d.ts +1 -0
  20. package/packages/typescript/src/services/message.d.ts.map +1 -1
  21. package/packages/typescript/src/services/message.js +328 -222
  22. package/packages/typescript/src/streaming-context.d.ts +9 -0
  23. package/packages/typescript/src/streaming-context.d.ts.map +1 -1
  24. package/packages/typescript/src/streaming-context.js +45 -0
  25. package/packages/typescript/src/trajectory-context.d.ts +6 -0
  26. package/packages/typescript/src/trajectory-context.d.ts.map +1 -1
  27. package/packages/typescript/src/types/events.d.ts +18 -1
  28. package/packages/typescript/src/types/events.d.ts.map +1 -1
  29. package/packages/typescript/src/types/events.js +2 -0
  30. package/packages/typescript/src/types/index.d.ts +4 -0
  31. package/packages/typescript/src/types/index.d.ts.map +1 -1
  32. package/packages/typescript/src/types/index.js +4 -0
  33. package/packages/typescript/src/types/pipeline-hooks.d.ts +234 -0
  34. package/packages/typescript/src/types/pipeline-hooks.d.ts.map +1 -0
  35. package/packages/typescript/src/types/pipeline-hooks.js +111 -0
  36. package/packages/typescript/src/types/prompt-optimization-hooks.d.ts +41 -0
  37. package/packages/typescript/src/types/prompt-optimization-hooks.d.ts.map +1 -0
  38. package/packages/typescript/src/types/prompt-optimization-hooks.js +1 -0
  39. package/packages/typescript/src/types/prompt-optimization-score-card.d.ts +22 -0
  40. package/packages/typescript/src/types/prompt-optimization-score-card.d.ts.map +1 -0
  41. package/packages/typescript/src/types/prompt-optimization-score-card.js +72 -0
  42. package/packages/typescript/src/types/prompt-optimization-trace.d.ts +53 -0
  43. package/packages/typescript/src/types/prompt-optimization-trace.d.ts.map +1 -0
  44. package/packages/typescript/src/types/prompt-optimization-trace.js +19 -0
  45. package/packages/typescript/src/types/runtime.d.ts +40 -1
  46. package/packages/typescript/src/types/runtime.d.ts.map +1 -1
@@ -1,14 +1,15 @@
1
1
  import { v4 } from "uuid";
2
2
  import { parseActionParams } from "../actions";
3
- import { createUniqueUuid } from "../entities";
4
3
  import { formatTaskCompletionStatus, getTaskCompletionCacheKey, } from "../features/advanced-capabilities/evaluators/task-completion";
4
+ import { createUniqueUuid } from "../entities";
5
5
  import { logger } from "../logger";
6
6
  import { imageDescriptionTemplate, messageHandlerTemplate, multiStepDecisionTemplate, multiStepSummaryTemplate, postActionDecisionTemplate, shouldRespondTemplate, } from "../prompts";
7
7
  import { isExplicitSelfModificationRequest } from "../should-respond";
8
- import { runWithStreamingContext } from "../streaming-context";
8
+ import { getModelStreamChunkDeliveryDepth, runWithStreamingContext, } from "../streaming-context";
9
9
  import { runWithTrajectoryContext, setTrajectoryPurpose, } from "../trajectory-context";
10
10
  import { EventType } from "../types/events";
11
11
  import { ModelType } from "../types/model";
12
+ import { incomingPipelineHookContext, modelStreamChunkPipelineHookContext, outgoingPipelineHookContext, parallelWithShouldRespondPipelineHookContext, preShouldRespondPipelineHookContext, } from "../types/pipeline-hooks";
12
13
  import { asUUID, ChannelType, ContentType } from "../types/primitives";
13
14
  import { composePromptFromState, getLocalServerUrl, parseBooleanFromText, parseKeyValueXml, truncateToCompleteSentence, } from "../utils";
14
15
  import { AVAILABLE_CONTEXTS_STATE_KEY, attachAvailableContexts, CONTEXT_ROUTING_STATE_KEY, mergeContextRouting, parseContextRoutingMetadata, setContextRoutingMetadata, } from "../utils/context-routing";
@@ -775,11 +776,18 @@ export class DefaultMessageService {
775
776
  : undefined;
776
777
  }
777
778
  return await runWithTrajectoryContext(typeof trajectoryStepId === "string" && trajectoryStepId.trim() !== ""
778
- ? { trajectoryStepId: trajectoryStepId.trim() }
779
+ ? {
780
+ trajectoryStepId: trajectoryStepId.trim(),
781
+ runId: runtime.getCurrentRunId?.(),
782
+ roomId: message.roomId,
783
+ messageId: message.id,
784
+ }
779
785
  : undefined, async () => {
780
786
  // Determine shouldRespondModel from options or runtime settings
781
787
  const shouldRespondModelSetting = runtime.getSetting("SHOULD_RESPOND_MODEL");
782
788
  const resolvedShouldRespondModel = normalizeShouldRespondModelType(options?.shouldRespondModel ?? shouldRespondModelSetting);
789
+ // Single ID used for tracking, streaming, and the final message (before opts / chunk wrapper).
790
+ const responseId = asUUID(v4());
783
791
  // WHY voice detection wraps onStreamChunk here instead of using a
784
792
  // separate ResponseStreamExtractor + AsyncLocalStorage context:
785
793
  //
@@ -815,6 +823,19 @@ export class DefaultMessageService {
815
823
  streamTextFallback += chunk;
816
824
  streamText = streamTextFallback;
817
825
  }
826
+ // Skip when this callback is invoked from `useModel`'s stream loop:
827
+ // `source: "use_model"` already ran for the same raw chunk (Node ALS).
828
+ if (getModelStreamChunkDeliveryDepth() === 0) {
829
+ await runtime.applyPipelineHooks("model_stream_chunk", modelStreamChunkPipelineHookContext({
830
+ source: "message_service",
831
+ chunk,
832
+ messageId,
833
+ roomId: message.roomId,
834
+ runId: runtime.getCurrentRunId(),
835
+ responseId,
836
+ accumulated,
837
+ }));
838
+ }
818
839
  // Only run first-sentence TTS detection when `accumulated` is present.
819
840
  // Raw-token streams (no accumulated) may contain XML markup or partial
820
841
  // structured output that would garble hasFirstSentence() and TTS.
@@ -956,8 +977,6 @@ export class DefaultMessageService {
956
977
  : undefined;
957
978
  // Set up timeout monitoring
958
979
  let timeoutId;
959
- // Single ID used for tracking, streaming, and the final message
960
- const responseId = asUUID(v4());
961
980
  try {
962
981
  runtime.logger.info({
963
982
  src: "service:message",
@@ -1266,12 +1285,10 @@ export class DefaultMessageService {
1266
1285
  mode: "none",
1267
1286
  };
1268
1287
  }
1269
- // Compose initial state
1270
- let state = await runtime.composeState(message, ["ENTITIES", "CHARACTER", "RECENT_MESSAGES", "ACTIONS"], true, false);
1271
- state = attachAvailableContexts(state, runtime);
1272
- // Get room and mention context
1288
+ // Room context for shouldRespond (fetch before compose so providers see
1289
+ // post-attachment and post-incoming-hook message state).
1273
1290
  const room = await runtime.getRoom(message.roomId);
1274
- // Process attachments before deciding to respond
1291
+ // Process attachments before state composition / incoming hooks
1275
1292
  if (message.content.attachments && message.content.attachments.length > 0) {
1276
1293
  message.content.attachments = await this.processAttachments(runtime, message.content.attachments);
1277
1294
  if (message.id) {
@@ -1284,169 +1301,94 @@ export class DefaultMessageService {
1284
1301
  });
1285
1302
  }
1286
1303
  }
1304
+ const preIncomingHookText = typeof message.content?.text === "string" ? message.content.text : "";
1305
+ await runtime.applyPipelineHooks("incoming_before_compose", incomingPipelineHookContext(message, {
1306
+ roomId: message.roomId,
1307
+ responseId,
1308
+ runId,
1309
+ }));
1310
+ const postIncomingHookText = typeof message.content?.text === "string" ? message.content.text : "";
1311
+ if (message.id && postIncomingHookText !== preIncomingHookText) {
1312
+ await runtime.updateMemory({
1313
+ id: message.id,
1314
+ content: message.content,
1315
+ });
1316
+ await runtime.queueEmbeddingGeneration({ ...message, id: message.id }, "normal");
1317
+ }
1287
1318
  const promptAttachments = resolvePromptAttachments(message.content.attachments);
1288
- let shouldRespondToMessage = true;
1289
- let terminalDecision = null;
1290
- let routedDecision = null;
1291
- let dualPressureLog = null;
1292
- let shouldRespondClassifierAction = null;
1319
+ // Compose initial state (after incoming hooks so providers/actions text matches this turn)
1320
+ let state = await runtime.composeState(message, ["ENTITIES", "CHARACTER", "RECENT_MESSAGES", "ACTIONS"], true, false);
1321
+ state = attachAvailableContexts(state, runtime);
1293
1322
  const metadata = typeof message.content.metadata === "object" &&
1294
1323
  message.content.metadata !== null
1295
1324
  ? message.content.metadata
1296
1325
  : null;
1297
1326
  const isAutonomous = metadata?.isAutonomous === true;
1298
1327
  const autonomyMode = typeof metadata?.autonomyMode === "string" ? metadata.autonomyMode : null;
1328
+ await runtime.applyPipelineHooks("pre_should_respond", preShouldRespondPipelineHookContext(message, {
1329
+ roomId: message.roomId,
1330
+ responseId,
1331
+ runId,
1332
+ state,
1333
+ isAutonomous,
1334
+ }));
1335
+ let shouldRespondToMessage = true;
1336
+ let terminalDecision = null;
1337
+ let routedDecision = null;
1338
+ let dualPressureLog = null;
1339
+ let shouldRespondClassifierAction = null;
1340
+ const parallelJoin = {};
1341
+ const setTranslatedUserText = (text) => {
1342
+ parallelJoin.translatedUserText = text;
1343
+ };
1344
+ const parallelHookCtx = parallelWithShouldRespondPipelineHookContext({
1345
+ roomId: message.roomId,
1346
+ responseId,
1347
+ runId,
1348
+ message,
1349
+ state,
1350
+ room: room ?? undefined,
1351
+ mentionContext,
1352
+ isAutonomous,
1353
+ setTranslatedUserText,
1354
+ });
1299
1355
  if (isAutonomous) {
1300
1356
  runtime.logger.debug({ src: "service:message", autonomyMode }, "Autonomy message bypassing shouldRespond checks");
1301
1357
  shouldRespondToMessage = true;
1358
+ await runtime.applyPipelineHooks("parallel_with_should_respond", parallelHookCtx);
1302
1359
  }
1303
1360
  else {
1304
- // Check if shouldRespond evaluation is enabled
1305
- const checkShouldRespondEnabled = runtime.isCheckShouldRespondEnabled();
1306
- // Determine if we should respond
1307
- const responseDecision = this.shouldRespond(runtime, message, room ?? undefined, mentionContext);
1308
- runtime.logger.debug({ src: "service:message", responseDecision, checkShouldRespondEnabled }, "Response decision");
1309
- // If checkShouldRespond is disabled, always respond (ChatGPT mode)
1310
- if (!checkShouldRespondEnabled) {
1311
- runtime.logger.debug({ src: "service:message" }, "checkShouldRespond disabled, always responding (ChatGPT mode)");
1312
- shouldRespondToMessage = true;
1313
- }
1314
- else if (responseDecision.skipEvaluation) {
1315
- // If we can skip the evaluation, use the decision directly
1316
- runtime.logger.debug({
1317
- src: "service:message",
1318
- agentName: runtime.character.name ?? "Agent",
1319
- reason: responseDecision.reason,
1320
- }, "Skipping LLM evaluation");
1321
- routedDecision = parseContextRoutingMetadata(responseDecision);
1322
- setContextRoutingMetadata(message, routedDecision);
1323
- shouldRespondToMessage = responseDecision.shouldRespond;
1324
- }
1325
- else {
1326
- state = {
1327
- ...state,
1328
- values: {
1329
- ...state.values,
1330
- dualPressureThreshold: resolveDualPressureThreshold(runtime),
1331
- },
1332
- };
1333
- const shouldRespondState = prepareShouldRespondState(state);
1334
- // Need LLM evaluation for ambiguous case
1335
- const _shouldRespondPrompt = composePromptFromState({
1336
- state: shouldRespondState,
1337
- template: runtime.character.templates?.shouldRespondTemplate ||
1338
- shouldRespondTemplate,
1339
- });
1340
- runtime.logger.debug({
1341
- src: "service:message",
1342
- agentName: runtime.character.name ?? "Agent",
1343
- reason: responseDecision.reason,
1344
- model: opts.shouldRespondModel,
1345
- }, "Using LLM evaluation");
1346
- // Use dynamicPromptExecFromState for structured output with validation
1347
- setTrajectoryPurpose("should_respond");
1348
- const responseObject = await runtime.dynamicPromptExecFromState({
1349
- state: shouldRespondState,
1350
- params: {
1351
- prompt: runtime.character.templates?.shouldRespondTemplate ||
1352
- shouldRespondTemplate,
1353
- ...(promptAttachments ? { attachments: promptAttachments } : {}),
1354
- },
1355
- schema: [
1356
- // Decision schema - no streaming, no per-field validation needed
1357
- // WHY: This is internal decision-making, not user-facing output
1358
- {
1359
- field: "name",
1360
- description: "The name of the agent responding",
1361
- validateField: false,
1362
- streamField: false,
1363
- },
1364
- {
1365
- field: "reasoning",
1366
- description: "Your reasoning for this decision",
1367
- validateField: false,
1368
- streamField: false,
1369
- },
1370
- {
1371
- field: "speak_up",
1372
- description: "Integer 0-100 pressure TO engage",
1373
- validateField: false,
1374
- streamField: false,
1375
- },
1376
- {
1377
- field: "hold_back",
1378
- description: "Integer 0-100 pressure to STAY QUIET",
1379
- validateField: false,
1380
- streamField: false,
1381
- },
1382
- {
1383
- field: "action",
1384
- description: "REPLY | RESPOND | IGNORE | STOP (REPLY and RESPOND both mean engage)",
1385
- validateField: false,
1386
- streamField: false,
1387
- },
1388
- {
1389
- field: "primaryContext",
1390
- description: "Primary domain context from available_contexts (e.g., wallet, knowledge)",
1391
- validateField: false,
1392
- streamField: false,
1393
- },
1394
- {
1395
- field: "secondaryContexts",
1396
- description: "Optional comma-separated additional domain contexts",
1397
- validateField: false,
1398
- streamField: false,
1399
- },
1400
- {
1401
- field: "evidenceTurnIds",
1402
- description: "Optional comma-separated message IDs that influenced this decision",
1403
- validateField: false,
1404
- streamField: false,
1405
- },
1406
- ],
1407
- options: {
1408
- contextCheckLevel: 0, // Set to 0 for now
1409
- // Classifier failures are usually transient provider hiccups;
1410
- // give this internal decision one short retry window.
1411
- maxRetries: Math.max(1, Math.min(opts.maxRetries, 2)),
1412
- retryBackoff: {
1413
- initialMs: 500,
1414
- multiplier: 2,
1415
- maxMs: 2000,
1416
- },
1417
- modelType: resolveShouldRespondModelType(opts.shouldRespondModel),
1418
- preferredEncapsulation: "toon",
1419
- },
1361
+ const [classifyOutcome] = await Promise.all([
1362
+ this.runNonAutonomousShouldRespondClassify(runtime, message, state, room ?? undefined, mentionContext, opts, promptAttachments),
1363
+ runtime.applyPipelineHooks("parallel_with_should_respond", parallelHookCtx),
1364
+ ]);
1365
+ shouldRespondToMessage = classifyOutcome.shouldRespondToMessage;
1366
+ terminalDecision = classifyOutcome.terminalDecision;
1367
+ routedDecision = classifyOutcome.routedDecision;
1368
+ dualPressureLog = classifyOutcome.dualPressureLog;
1369
+ shouldRespondClassifierAction =
1370
+ classifyOutcome.shouldRespondClassifierAction;
1371
+ state = classifyOutcome.state;
1372
+ }
1373
+ const joinedTranslation = typeof parallelJoin.translatedUserText === "string"
1374
+ ? parallelJoin.translatedUserText
1375
+ : undefined;
1376
+ if (joinedTranslation !== undefined &&
1377
+ joinedTranslation !== message.content.text) {
1378
+ message.content.text = joinedTranslation;
1379
+ if (message.id) {
1380
+ await runtime.updateMemory({
1381
+ id: message.id,
1382
+ content: message.content,
1420
1383
  });
1421
- runtime.logger.debug({ src: "service:message", responseObject }, "Parsed evaluation result");
1422
- const rawAction = typeof responseObject?.action === "string"
1423
- ? responseObject.action
1424
- : "";
1425
- const actionUpper = rawAction.trim().toUpperCase();
1426
- const hasValidClassifierAction = actionUpper.length > 0 && ALLOWED_CLASSIFIER_ACTIONS.has(actionUpper);
1427
- routedDecision = parseContextRoutingMetadata(responseObject);
1428
- setContextRoutingMetadata(message, routedDecision);
1429
- if (!hasValidClassifierAction) {
1430
- runtime.logger.warn({
1431
- src: "service:message",
1432
- action: responseObject?.action,
1433
- }, "Classifier response missing valid action; treating as IGNORE");
1434
- terminalDecision = "IGNORE";
1435
- shouldRespondToMessage = false;
1436
- }
1437
- else {
1438
- const dual = applyDualPressureToClassifierAction(runtime, responseObject, rawAction);
1439
- dualPressureLog = dual.pressure;
1440
- shouldRespondClassifierAction = dual.finalActionUpper;
1441
- if (dual.finalActionUpper === "IGNORE" ||
1442
- dual.finalActionUpper === "STOP") {
1443
- terminalDecision = dual.finalActionUpper;
1444
- }
1445
- shouldRespondToMessage =
1446
- dual.finalActionUpper === "REPLY" ||
1447
- dual.finalActionUpper === "RESPOND";
1448
- }
1384
+ await runtime.queueEmbeddingGeneration({ ...message, id: message.id }, "normal");
1385
+ }
1386
+ if (message.id) {
1387
+ runtime.stateCache.delete(message.id);
1388
+ runtime.stateCache.delete(`${message.id}_action_results`);
1449
1389
  }
1390
+ state = await runtime.composeState(message, ["ENTITIES", "CHARACTER", "RECENT_MESSAGES", "ACTIONS"], true, false);
1391
+ state = attachAvailableContexts(state, runtime);
1450
1392
  }
1451
1393
  let responseContent = null;
1452
1394
  let responseMessages = [];
@@ -1501,8 +1443,8 @@ export class DefaultMessageService {
1501
1443
  if (responseContent?.providers && responseContent.providers.length > 0) {
1502
1444
  state = await runtime.composeState(message, responseContent.providers, false, false);
1503
1445
  }
1504
- // Save response memory to database
1505
- if (responseMessages.length > 0) {
1446
+ // Save response memory to database (simple mode persists after hooks; see below)
1447
+ if (responseMessages.length > 0 && mode !== "simple") {
1506
1448
  for (const responseMemory of responseMessages) {
1507
1449
  // Update the content in case inReplyTo was added
1508
1450
  if (responseContent) {
@@ -1511,11 +1453,6 @@ export class DefaultMessageService {
1511
1453
  runtime.logger.debug({ src: "service:message", memoryId: responseMemory.id }, "Saving response to memory");
1512
1454
  await runtime.createMemory(responseMemory, "messages");
1513
1455
  await this.emitMessageSent(runtime, responseMemory, message.content.source ?? "messageHandler");
1514
- // Track IDs for simple-mode so we can delete orphaned
1515
- // memories if reflection overrides the deferred emit.
1516
- if (mode === "simple" && responseMemory.id) {
1517
- pendingSimpleMemoryIds.push(responseMemory.id);
1518
- }
1519
1456
  }
1520
1457
  }
1521
1458
  if (responseContent) {
@@ -1528,11 +1465,25 @@ export class DefaultMessageService {
1528
1465
  providers: responseContent.providers,
1529
1466
  }, "Simple response used providers");
1530
1467
  }
1531
- // Defer the callback until after reflection has had a
1532
- // chance to override. Redact secrets now so the content
1533
- // is ready to flush at the bottom of handleMessage.
1534
- if (responseContent.text) {
1535
- responseContent.text = runtime.redactSecrets(responseContent.text);
1468
+ // WHY order: hooks createMemory deferred callback matches wire + DB.
1469
+ await runtime.applyPipelineHooks("outgoing_before_deliver", outgoingPipelineHookContext(responseContent, {
1470
+ source: "simple",
1471
+ roomId: message.roomId,
1472
+ message,
1473
+ responseId: responseContent.responseId ?? responseMessages[0]?.id,
1474
+ }));
1475
+ if (responseMessages.length > 0) {
1476
+ for (const responseMemory of responseMessages) {
1477
+ if (responseContent) {
1478
+ responseMemory.content = responseContent;
1479
+ }
1480
+ runtime.logger.debug({ src: "service:message", memoryId: responseMemory.id }, "Saving response to memory");
1481
+ await runtime.createMemory(responseMemory, "messages");
1482
+ await this.emitMessageSent(runtime, responseMemory, message.content.source ?? "messageHandler");
1483
+ if (responseMemory.id) {
1484
+ pendingSimpleMemoryIds.push(responseMemory.id);
1485
+ }
1486
+ }
1536
1487
  }
1537
1488
  pendingSimpleEmit = responseContent;
1538
1489
  }
@@ -1609,11 +1560,11 @@ export class DefaultMessageService {
1609
1560
  simple: true,
1610
1561
  inReplyTo: createUniqueUuid(runtime, message.id),
1611
1562
  };
1612
- // Call the callback with the terminal content
1613
- if (callback) {
1614
- await callback(terminalContent);
1615
- }
1616
- // Save this terminal action/thought to memory
1563
+ await runtime.applyPipelineHooks("outgoing_before_deliver", outgoingPipelineHookContext(terminalContent, {
1564
+ source: "excluded",
1565
+ roomId: message.roomId,
1566
+ message,
1567
+ }));
1617
1568
  const terminalMemory = {
1618
1569
  id: asUUID(v4()),
1619
1570
  entityId: runtime.agentId,
@@ -1625,6 +1576,9 @@ export class DefaultMessageService {
1625
1576
  await runtime.createMemory(terminalMemory, "messages");
1626
1577
  await this.emitMessageSent(runtime, terminalMemory, message.content.source ?? "messageHandler");
1627
1578
  runtime.logger.debug({ src: "service:message", memoryId: terminalMemory.id }, "Saved terminal response to memory");
1579
+ if (callback) {
1580
+ await callback(terminalContent);
1581
+ }
1628
1582
  }
1629
1583
  // Clean up the response ID
1630
1584
  clearLatestResponseId(runtime.agentId, message.roomId, responseId);
@@ -1636,9 +1590,12 @@ export class DefaultMessageService {
1636
1590
  responseContent.evalCallbacks = content;
1637
1591
  }
1638
1592
  if (callback) {
1639
- if (content.text) {
1640
- content.text = runtime.redactSecrets(content.text);
1641
- }
1593
+ await runtime.applyPipelineHooks("outgoing_before_deliver", outgoingPipelineHookContext(content, {
1594
+ source: "evaluate",
1595
+ roomId: message.roomId,
1596
+ message,
1597
+ responseId: content.responseId,
1598
+ }));
1642
1599
  return callback(content);
1643
1600
  }
1644
1601
  return [];
@@ -1794,6 +1751,156 @@ export class DefaultMessageService {
1794
1751
  : {}),
1795
1752
  };
1796
1753
  }
1754
+ async runNonAutonomousShouldRespondClassify(runtime, message, state, room, mentionContext, opts, promptAttachments) {
1755
+ let shouldRespondToMessage = true;
1756
+ let terminalDecision = null;
1757
+ let routedDecision = null;
1758
+ let dualPressureLog = null;
1759
+ let shouldRespondClassifierAction = null;
1760
+ let workingState = state;
1761
+ const checkShouldRespondEnabled = runtime.isCheckShouldRespondEnabled();
1762
+ const responseDecision = this.shouldRespond(runtime, message, room, mentionContext);
1763
+ runtime.logger.debug({ src: "service:message", responseDecision, checkShouldRespondEnabled }, "Response decision");
1764
+ if (!checkShouldRespondEnabled) {
1765
+ runtime.logger.debug({ src: "service:message" }, "checkShouldRespond disabled, always responding (ChatGPT mode)");
1766
+ shouldRespondToMessage = true;
1767
+ }
1768
+ else if (responseDecision.skipEvaluation) {
1769
+ runtime.logger.debug({
1770
+ src: "service:message",
1771
+ agentName: runtime.character.name ?? "Agent",
1772
+ reason: responseDecision.reason,
1773
+ }, "Skipping LLM evaluation");
1774
+ routedDecision = parseContextRoutingMetadata(responseDecision);
1775
+ setContextRoutingMetadata(message, routedDecision);
1776
+ shouldRespondToMessage = responseDecision.shouldRespond;
1777
+ }
1778
+ else {
1779
+ workingState = {
1780
+ ...workingState,
1781
+ values: {
1782
+ ...workingState.values,
1783
+ dualPressureThreshold: resolveDualPressureThreshold(runtime),
1784
+ },
1785
+ };
1786
+ const shouldRespondState = prepareShouldRespondState(workingState);
1787
+ const _shouldRespondPrompt = composePromptFromState({
1788
+ state: shouldRespondState,
1789
+ template: runtime.character.templates?.shouldRespondTemplate ||
1790
+ shouldRespondTemplate,
1791
+ });
1792
+ runtime.logger.debug({
1793
+ src: "service:message",
1794
+ agentName: runtime.character.name ?? "Agent",
1795
+ reason: responseDecision.reason,
1796
+ model: opts.shouldRespondModel,
1797
+ }, "Using LLM evaluation");
1798
+ setTrajectoryPurpose("should_respond");
1799
+ const responseObject = await runtime.dynamicPromptExecFromState({
1800
+ state: shouldRespondState,
1801
+ params: {
1802
+ prompt: runtime.character.templates?.shouldRespondTemplate ||
1803
+ shouldRespondTemplate,
1804
+ ...(promptAttachments ? { attachments: promptAttachments } : {}),
1805
+ },
1806
+ schema: [
1807
+ {
1808
+ field: "name",
1809
+ description: "The name of the agent responding",
1810
+ validateField: false,
1811
+ streamField: false,
1812
+ },
1813
+ {
1814
+ field: "reasoning",
1815
+ description: "Your reasoning for this decision",
1816
+ validateField: false,
1817
+ streamField: false,
1818
+ },
1819
+ {
1820
+ field: "speak_up",
1821
+ description: "Integer 0-100 pressure TO engage",
1822
+ validateField: false,
1823
+ streamField: false,
1824
+ },
1825
+ {
1826
+ field: "hold_back",
1827
+ description: "Integer 0-100 pressure to STAY QUIET",
1828
+ validateField: false,
1829
+ streamField: false,
1830
+ },
1831
+ {
1832
+ field: "action",
1833
+ description: "REPLY | RESPOND | IGNORE | STOP (REPLY and RESPOND both mean engage)",
1834
+ validateField: false,
1835
+ streamField: false,
1836
+ },
1837
+ {
1838
+ field: "primaryContext",
1839
+ description: "Primary domain context from available_contexts (e.g., wallet, knowledge)",
1840
+ validateField: false,
1841
+ streamField: false,
1842
+ },
1843
+ {
1844
+ field: "secondaryContexts",
1845
+ description: "Optional comma-separated additional domain contexts",
1846
+ validateField: false,
1847
+ streamField: false,
1848
+ },
1849
+ {
1850
+ field: "evidenceTurnIds",
1851
+ description: "Optional comma-separated message IDs that influenced this decision",
1852
+ validateField: false,
1853
+ streamField: false,
1854
+ },
1855
+ ],
1856
+ options: {
1857
+ contextCheckLevel: 0,
1858
+ maxRetries: Math.max(1, Math.min(opts.maxRetries, 2)),
1859
+ retryBackoff: {
1860
+ initialMs: 500,
1861
+ multiplier: 2,
1862
+ maxMs: 2000,
1863
+ },
1864
+ modelType: resolveShouldRespondModelType(opts.shouldRespondModel),
1865
+ preferredEncapsulation: "toon",
1866
+ },
1867
+ });
1868
+ runtime.logger.debug({ src: "service:message", responseObject }, "Parsed evaluation result");
1869
+ const rawAction = typeof responseObject?.action === "string" ? responseObject.action : "";
1870
+ const actionUpper = rawAction.trim().toUpperCase();
1871
+ const hasValidClassifierAction = actionUpper.length > 0 && ALLOWED_CLASSIFIER_ACTIONS.has(actionUpper);
1872
+ routedDecision = parseContextRoutingMetadata(responseObject);
1873
+ setContextRoutingMetadata(message, routedDecision);
1874
+ if (!hasValidClassifierAction) {
1875
+ runtime.logger.warn({
1876
+ src: "service:message",
1877
+ action: responseObject?.action,
1878
+ }, "Classifier response missing valid action; treating as IGNORE");
1879
+ terminalDecision = "IGNORE";
1880
+ shouldRespondToMessage = false;
1881
+ }
1882
+ else {
1883
+ const dual = applyDualPressureToClassifierAction(runtime, responseObject, rawAction);
1884
+ dualPressureLog = dual.pressure;
1885
+ shouldRespondClassifierAction = dual.finalActionUpper;
1886
+ if (dual.finalActionUpper === "IGNORE" ||
1887
+ dual.finalActionUpper === "STOP") {
1888
+ terminalDecision = dual.finalActionUpper;
1889
+ }
1890
+ shouldRespondToMessage =
1891
+ dual.finalActionUpper === "REPLY" ||
1892
+ dual.finalActionUpper === "RESPOND";
1893
+ }
1894
+ }
1895
+ return {
1896
+ shouldRespondToMessage,
1897
+ terminalDecision,
1898
+ routedDecision,
1899
+ dualPressureLog,
1900
+ shouldRespondClassifierAction,
1901
+ state: workingState,
1902
+ };
1903
+ }
1797
1904
  /**
1798
1905
  * Determines whether the agent should respond to a message.
1799
1906
  * Uses simple rules for obvious cases (DM, mentions) and defers to LLM for ambiguous cases.
@@ -2135,7 +2242,8 @@ export class DefaultMessageService {
2135
2242
  accumulatedState = withActionResults(continuation.state, traceActionResults);
2136
2243
  }
2137
2244
  accumulatedState = withTaskCompletion(accumulatedState, taskCompletion);
2138
- if (continuation.responseMessages.length > 0) {
2245
+ if (continuation.responseMessages.length > 0 &&
2246
+ continuation.mode !== "simple") {
2139
2247
  for (const responseMemory of continuation.responseMessages) {
2140
2248
  responseMemory.content = responseContent;
2141
2249
  await runtime.createMemory(responseMemory, "messages");
@@ -2144,10 +2252,22 @@ export class DefaultMessageService {
2144
2252
  responseMessages.push(...continuation.responseMessages);
2145
2253
  }
2146
2254
  if (continuation.mode === "simple") {
2147
- if (callback) {
2148
- if (responseContent.text) {
2149
- responseContent.text = runtime.redactSecrets(responseContent.text);
2255
+ await runtime.applyPipelineHooks("outgoing_before_deliver", outgoingPipelineHookContext(responseContent, {
2256
+ source: "continuation_simple",
2257
+ roomId: message.roomId,
2258
+ message,
2259
+ responseId: responseContent.responseId ??
2260
+ continuation.responseMessages[0]?.id,
2261
+ }));
2262
+ if (continuation.responseMessages.length > 0) {
2263
+ for (const responseMemory of continuation.responseMessages) {
2264
+ responseMemory.content = responseContent;
2265
+ await runtime.createMemory(responseMemory, "messages");
2266
+ await this.emitMessageSent(runtime, responseMemory, message.content.source ?? "messageHandler");
2150
2267
  }
2268
+ responseMessages.push(...continuation.responseMessages);
2269
+ }
2270
+ if (callback) {
2151
2271
  await callback(responseContent);
2152
2272
  }
2153
2273
  break;
@@ -2218,7 +2338,8 @@ export class DefaultMessageService {
2218
2338
  else {
2219
2339
  accumulatedState = withTaskCompletion(withActionResults(continuation.state, initialActionResults), taskCompletion);
2220
2340
  }
2221
- if (continuation.responseMessages.length > 0) {
2341
+ if (continuation.responseMessages.length > 0 &&
2342
+ continuation.mode !== "simple") {
2222
2343
  for (const responseMemory of continuation.responseMessages) {
2223
2344
  responseMemory.content = responseContent;
2224
2345
  await runtime.createMemory(responseMemory, "messages");
@@ -2227,10 +2348,21 @@ export class DefaultMessageService {
2227
2348
  responseMessages.push(...continuation.responseMessages);
2228
2349
  }
2229
2350
  if (continuation.mode === "simple") {
2230
- if (callback) {
2231
- if (responseContent.text) {
2232
- responseContent.text = runtime.redactSecrets(responseContent.text);
2351
+ await runtime.applyPipelineHooks("outgoing_before_deliver", outgoingPipelineHookContext(responseContent, {
2352
+ source: "continuation_simple",
2353
+ roomId: message.roomId,
2354
+ message,
2355
+ responseId: responseContent.responseId ?? continuation.responseMessages[0]?.id,
2356
+ }));
2357
+ if (continuation.responseMessages.length > 0) {
2358
+ for (const responseMemory of continuation.responseMessages) {
2359
+ responseMemory.content = responseContent;
2360
+ await runtime.createMemory(responseMemory, "messages");
2361
+ await this.emitMessageSent(runtime, responseMemory, message.content.source ?? "messageHandler");
2233
2362
  }
2363
+ responseMessages.push(...continuation.responseMessages);
2364
+ }
2365
+ if (callback) {
2234
2366
  await callback(responseContent);
2235
2367
  }
2236
2368
  return {
@@ -2285,38 +2417,6 @@ export class DefaultMessageService {
2285
2417
  if (!state.values?.actionNames) {
2286
2418
  runtime.logger.warn({ src: "service:message" }, "actionNames data missing from state");
2287
2419
  }
2288
- // Self-modification requests already have a deterministic action path.
2289
- // Skip the planner so group-chat style updates reliably hit MODIFY_CHARACTER.
2290
- if (isExplicitSelfModificationRequest(message.content.text || "")) {
2291
- const modifyCharacterAction = runtime.actions.find((action) => action.name === "MODIFY_CHARACTER");
2292
- const canHandleSelfModification = await modifyCharacterAction?.validate?.(runtime, message, state);
2293
- if (canHandleSelfModification) {
2294
- const responseContent = {
2295
- thought: "Directly route explicit self-modification request to MODIFY_CHARACTER.",
2296
- actions: ["MODIFY_CHARACTER"],
2297
- providers: [],
2298
- text: "",
2299
- simple: false,
2300
- responseId,
2301
- };
2302
- const responseMessages = [
2303
- {
2304
- id: responseId,
2305
- entityId: runtime.agentId,
2306
- agentId: runtime.agentId,
2307
- content: responseContent,
2308
- roomId: message.roomId,
2309
- createdAt: Date.now(),
2310
- },
2311
- ];
2312
- return {
2313
- responseContent,
2314
- responseMessages,
2315
- state,
2316
- mode: "actions",
2317
- };
2318
- }
2319
- }
2320
2420
  let responseContent = null;
2321
2421
  // Create streaming context for retry state tracking
2322
2422
  const streamingExtractor = opts.onStreamChunk
@@ -2701,7 +2801,7 @@ Output ONLY the continuation, starting immediately after the last character abov
2701
2801
  .filter(Boolean)
2702
2802
  .join(" ");
2703
2803
  }
2704
- replyText = runtime.redactSecrets(truncateToCompleteSentence(replyText.trim(), 2000));
2804
+ replyText = truncateToCompleteSentence(replyText.trim(), 2000);
2705
2805
  const responseContent = {
2706
2806
  thought: `Explain the structured-output failure during ${stage}.`,
2707
2807
  actions: ["REPLY"],
@@ -2915,11 +3015,17 @@ Output ONLY the continuation, starting immediately after the last character abov
2915
3015
  completedProviders: Array.from(completedProviders),
2916
3016
  }, `Providers took too long (>${PROVIDERS_TOTAL_TIMEOUT_MS}ms) - slow providers: ${pendingProviders.join(", ")}`);
2917
3017
  if (callback) {
2918
- await callback({
3018
+ const timeoutContent = {
2919
3019
  text: "Providers took too long to respond. Please optimize your providers or use caching.",
2920
3020
  actions: [],
2921
3021
  thought: "Provider timeout - pipeline aborted",
2922
- });
3022
+ };
3023
+ await runtime.applyPipelineHooks("outgoing_before_deliver", outgoingPipelineHookContext(timeoutContent, {
3024
+ source: "simple",
3025
+ roomId: message.roomId,
3026
+ message,
3027
+ }));
3028
+ await callback(timeoutContent);
2923
3029
  }
2924
3030
  return {
2925
3031
  responseContent: null,