@cereworker/core 26.403.2 → 26.404.2

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.
@@ -12,10 +12,18 @@ const log = createLogger('orchestrator');
12
12
  const TASK_COMPLETE_TOOL = 'task_complete';
13
13
  const TASK_BLOCKED_TOOL = 'task_blocked';
14
14
  const TASK_CHECKPOINT_TOOL = 'task_checkpoint';
15
+ const TASK_UPSERT_TOOL = 'task_upsert';
16
+ const TASK_REMOVE_TOOL = 'task_remove';
17
+ const TASK_LIST_TOOL = 'task_list';
18
+ const TASK_GET_TOOL = 'task_get';
15
19
  const INTERNAL_TASK_TOOL_NAMES = new Set([
16
20
  TASK_COMPLETE_TOOL,
17
21
  TASK_BLOCKED_TOOL,
18
22
  TASK_CHECKPOINT_TOOL,
23
+ TASK_UPSERT_TOOL,
24
+ TASK_REMOVE_TOOL,
25
+ TASK_LIST_TOOL,
26
+ TASK_GET_TOOL,
19
27
  ]);
20
28
  const SYSTEM_FALLBACK_COMPLETION_PROMPT = '[System fallback] The last turn ended without a final answer. Continue from the last verified state and end by calling task_complete or task_blocked before your final answer.';
21
29
  const SYSTEM_FALLBACK_STALL_PROMPT = '[System fallback] The stalled turn is being retried from the last verified state.';
@@ -89,6 +97,7 @@ export class Orchestrator extends TypedEventEmitter {
89
97
  streamPhase = 'idle';
90
98
  activeToolCall = null;
91
99
  currentStreamTurn = null;
100
+ currentQuerySession = null;
92
101
  currentAttemptCompletionState = null;
93
102
  currentPartialContent = '';
94
103
  currentLastContentKind = 'empty';
@@ -377,6 +386,8 @@ export class Orchestrator extends TypedEventEmitter {
377
386
  conversationId: options?.conversationId,
378
387
  sessionKey: options?.sessionKey,
379
388
  scopeKey: options?.scopeKey,
389
+ turnId: options?.turnId,
390
+ attempt: options?.attempt,
380
391
  });
381
392
  }
382
393
  unregisterTool(name) {
@@ -613,6 +624,67 @@ export class Orchestrator extends TypedEventEmitter {
613
624
  return [];
614
625
  return this.conversations.getMessages(id);
615
626
  }
627
+ getQuerySession(conversationId, sessionId) {
628
+ return this.conversations.getQuerySession(conversationId, sessionId);
629
+ }
630
+ getSessionEvents(conversationId, sessionId) {
631
+ return this.conversations.getSessionEvents(conversationId, sessionId);
632
+ }
633
+ recordSessionEvent(conversationId, sessionId, type, summary, data) {
634
+ const session = this.conversations.getQuerySession(conversationId, sessionId);
635
+ if (!session)
636
+ return;
637
+ const updatedAt = Date.now();
638
+ const updatedSession = {
639
+ ...session,
640
+ updatedAt,
641
+ summary: this.truncateResumeText(summary, 500),
642
+ };
643
+ this.conversations.saveQuerySession(conversationId, updatedSession);
644
+ const event = {
645
+ sessionId,
646
+ conversationId,
647
+ turnId: session.turnId,
648
+ attempt: session.attempt,
649
+ timestamp: updatedAt,
650
+ type,
651
+ state: updatedSession.state,
652
+ summary: this.truncateResumeText(summary, 500),
653
+ instanceId: updatedSession.instanceId,
654
+ checkpointPath: updatedSession.checkpointPath ?? null,
655
+ data,
656
+ };
657
+ this.conversations.appendSessionEvent(conversationId, sessionId, event);
658
+ }
659
+ recordSessionMemoryUpdate(conversationId, sessionId, snapshot) {
660
+ const session = this.conversations.getQuerySession(conversationId, sessionId);
661
+ if (!session)
662
+ return;
663
+ const updated = {
664
+ ...session,
665
+ memory: snapshot,
666
+ updatedAt: Date.now(),
667
+ summary: snapshot.summary || session.summary,
668
+ };
669
+ this.conversations.saveQuerySession(conversationId, updated);
670
+ const event = {
671
+ sessionId,
672
+ conversationId,
673
+ turnId: session.turnId,
674
+ attempt: session.attempt,
675
+ timestamp: snapshot.updatedAt,
676
+ type: 'memory_updated',
677
+ state: updated.state,
678
+ summary: this.truncateResumeText(`Session memory updated: ${snapshot.summary}`, 500),
679
+ instanceId: updated.instanceId,
680
+ checkpointPath: updated.checkpointPath ?? null,
681
+ data: {
682
+ excerpt: snapshot.excerpt,
683
+ },
684
+ };
685
+ this.conversations.appendSessionEvent(conversationId, sessionId, event);
686
+ this.emit({ type: 'session:memory-updated', conversationId, sessionId, snapshot });
687
+ }
616
688
  startConversation() {
617
689
  const conversation = this.conversations.create();
618
690
  this.activeConversationId = conversation.id;
@@ -749,6 +821,105 @@ export class Orchestrator extends TypedEventEmitter {
749
821
  this.currentLastContentKind = 'empty';
750
822
  this.currentJournaledContentLength = 0;
751
823
  }
824
+ createQuerySession(conversationId, turnId, attempt, source, latestUserMessage, stallRetryCount, completionRetryCount, priorSession) {
825
+ const timestamp = Date.now();
826
+ const instance = this.instanceStore?.get();
827
+ return {
828
+ id: turnId,
829
+ conversationId,
830
+ turnId,
831
+ attempt,
832
+ source,
833
+ state: 'ready',
834
+ startedAt: timestamp,
835
+ updatedAt: timestamp,
836
+ summary: `Turn attempt ${attempt} started.`,
837
+ latestUserMessage: this.truncateResumeText(latestUserMessage, 1200),
838
+ stallRetryCount,
839
+ completionRetryCount,
840
+ instanceId: instance?.id,
841
+ checkpointPath: instance?.activeCheckpoint ?? null,
842
+ // Carry forward recovery state from prior attempt for crash recovery
843
+ ...(priorSession?.latestBoundary ? { latestBoundary: priorSession.latestBoundary } : {}),
844
+ ...(priorSession?.lastOutcome ? { lastOutcome: priorSession.lastOutcome } : {}),
845
+ ...(priorSession?.lastError ? { lastError: priorSession.lastError } : {}),
846
+ };
847
+ }
848
+ saveCurrentQuerySession() {
849
+ if (!this.currentStreamTurn || !this.currentQuerySession)
850
+ return;
851
+ this.conversations.saveQuerySession(this.currentStreamTurn.conversationId, this.currentQuerySession);
852
+ }
853
+ updateCurrentQuerySession(type, summary, data) {
854
+ if (!this.currentStreamTurn || !this.currentQuerySession)
855
+ return;
856
+ const eventState = this.getQuerySessionState(type, data);
857
+ const updatedAt = Date.now();
858
+ const next = {
859
+ ...this.currentQuerySession,
860
+ attempt: this.currentStreamTurn.attempt,
861
+ updatedAt,
862
+ summary: this.truncateResumeText(summary, 500),
863
+ state: this.resolveQuerySessionState(type, eventState, data),
864
+ checkpointPath: this.instanceStore?.get()?.activeCheckpoint ?? this.currentQuerySession.checkpointPath ?? null,
865
+ };
866
+ if (type === 'partial_text') {
867
+ const excerpt = typeof data?.excerpt === 'string' ? data.excerpt : summary;
868
+ next.latestAssistantMessage = this.truncateResumeText(excerpt, 1200);
869
+ }
870
+ if (type === 'tool_start') {
871
+ next.activeToolName = typeof data?.toolName === 'string' ? data.toolName : undefined;
872
+ next.activeToolCallId = typeof data?.callId === 'string' ? data.callId : undefined;
873
+ }
874
+ else if (type !== 'tool_end') {
875
+ next.activeToolName = undefined;
876
+ next.activeToolCallId = undefined;
877
+ }
878
+ if (type === 'tool_end') {
879
+ next.activeToolName = undefined;
880
+ next.activeToolCallId = undefined;
881
+ }
882
+ if (type === 'turn_finished') {
883
+ const finalContent = typeof data?.finalContent === 'string' ? data.finalContent.trim() : '';
884
+ if (finalContent) {
885
+ next.latestAssistantMessage = this.truncateResumeText(finalContent, 1200);
886
+ }
887
+ if (typeof data?.turnOutcome === 'string') {
888
+ next.lastOutcome = data.turnOutcome;
889
+ }
890
+ }
891
+ if (type === 'turn_error') {
892
+ next.lastError = typeof data?.error === 'string' ? data.error : summary;
893
+ if (data?.aborted === true) {
894
+ next.lastOutcome = 'aborted';
895
+ }
896
+ else {
897
+ next.lastOutcome = 'protocol_error';
898
+ }
899
+ }
900
+ if (type === 'completion_signal' && typeof data?.signal === 'string') {
901
+ next.summary = this.truncateResumeText(summary, 500);
902
+ }
903
+ this.currentQuerySession = next;
904
+ this.saveCurrentQuerySession();
905
+ }
906
+ resolveQuerySessionState(type, defaultState, data) {
907
+ if (type === 'turn_finished') {
908
+ const turnOutcome = data?.turnOutcome;
909
+ if (turnOutcome === 'completed' || turnOutcome === 'completed_no_text')
910
+ return 'completed';
911
+ if (turnOutcome === 'stalled')
912
+ return 'stalled';
913
+ if (turnOutcome === 'aborted')
914
+ return 'aborted';
915
+ if (turnOutcome === 'ended_on_tool_calls' || turnOutcome === 'completion_signal_missing') {
916
+ return 'waiting_followup';
917
+ }
918
+ if (turnOutcome === 'protocol_error')
919
+ return 'failed';
920
+ }
921
+ return defaultState;
922
+ }
752
923
  appendTurnJournalEntry(type, summary, data) {
753
924
  if (!this.currentStreamTurn)
754
925
  return;
@@ -761,6 +932,70 @@ export class Orchestrator extends TypedEventEmitter {
761
932
  ...(data ? { data } : {}),
762
933
  };
763
934
  this.conversations.appendTurnJournalEntry(this.currentStreamTurn.conversationId, this.currentStreamTurn.turnId, entry);
935
+ this.appendSessionEvent(type, entry.summary, data);
936
+ }
937
+ appendSessionEvent(type, summary, data) {
938
+ if (!this.currentStreamTurn)
939
+ return;
940
+ const instance = this.instanceStore?.get();
941
+ const event = {
942
+ sessionId: this.currentStreamTurn.sessionId,
943
+ conversationId: this.currentStreamTurn.conversationId,
944
+ turnId: this.currentStreamTurn.turnId,
945
+ attempt: this.currentStreamTurn.attempt,
946
+ timestamp: Date.now(),
947
+ type: this.mapJournalEntryToSessionEvent(type),
948
+ state: this.getQuerySessionState(type, data),
949
+ summary: this.truncateResumeText(summary, 500),
950
+ instanceId: instance?.id,
951
+ checkpointPath: instance?.activeCheckpoint ?? null,
952
+ ...(data ? { data } : {}),
953
+ };
954
+ this.conversations.appendSessionEvent(this.currentStreamTurn.conversationId, this.currentStreamTurn.sessionId, event);
955
+ this.updateCurrentQuerySession(type, summary, data);
956
+ }
957
+ mapJournalEntryToSessionEvent(type) {
958
+ switch (type) {
959
+ case 'tool_start':
960
+ return 'tool_started';
961
+ case 'tool_end':
962
+ return 'tool_finished';
963
+ case 'checkpoint':
964
+ return 'checkpoint_recorded';
965
+ case 'boundary':
966
+ return 'boundary_committed';
967
+ case 'completion_signal':
968
+ return 'completion_signal_recorded';
969
+ case 'recovery':
970
+ return 'recovery_assessed';
971
+ case 'turn_error':
972
+ return 'turn_failed';
973
+ default:
974
+ return type;
975
+ }
976
+ }
977
+ getQuerySessionState(type, data) {
978
+ switch (type) {
979
+ case 'turn_started':
980
+ return 'ready';
981
+ case 'partial_text':
982
+ return 'sampling';
983
+ case 'tool_start':
984
+ return 'tool_execution';
985
+ case 'tool_end':
986
+ case 'checkpoint':
987
+ case 'boundary':
988
+ case 'completion_signal':
989
+ return 'waiting_followup';
990
+ case 'recovery':
991
+ return data?.cause === 'stall' ? 'stalled' : 'waiting_followup';
992
+ case 'turn_finished':
993
+ return 'completed';
994
+ case 'turn_error':
995
+ return data?.aborted ? 'aborted' : 'failed';
996
+ default:
997
+ return 'waiting_followup';
998
+ }
764
999
  }
765
1000
  pruneTurnJournals(conversationId) {
766
1001
  try {
@@ -817,6 +1052,14 @@ export class Orchestrator extends TypedEventEmitter {
817
1052
  evidence: summary.evidence,
818
1053
  browserState: summary.browserState,
819
1054
  });
1055
+ if (this.currentQuerySession) {
1056
+ this.currentQuerySession = {
1057
+ ...this.currentQuerySession,
1058
+ latestBoundary: summary,
1059
+ updatedAt: Date.now(),
1060
+ };
1061
+ this.saveCurrentQuerySession();
1062
+ }
820
1063
  return summary;
821
1064
  }
822
1065
  deriveRepetitionSignals(continuity) {
@@ -1606,7 +1849,7 @@ export class Orchestrator extends TypedEventEmitter {
1606
1849
  }
1607
1850
  this.resetStreamState();
1608
1851
  }
1609
- async sendMessage(content, conversationId) {
1852
+ async sendMessage(content, conversationId, options) {
1610
1853
  if (!this.cerebrum)
1611
1854
  throw new Error('Cerebrum not connected');
1612
1855
  if (this.cerebellum && !this.cerebellum.isConnected()) {
@@ -1629,7 +1872,7 @@ export class Orchestrator extends TypedEventEmitter {
1629
1872
  let nextRetryContext = null;
1630
1873
  // Track message IDs from failed attempts so they can be excluded from retries and cleaned up.
1631
1874
  const failedAttemptMessageIds = [];
1632
- const turnId = nanoid(10);
1875
+ const turnId = options?.turnId ?? nanoid(10);
1633
1876
  const maxTotalAttempts = 1 + this.maxNudgeRetries + this.maxCompletionRetries;
1634
1877
  let loopTerminated = false;
1635
1878
  let nextRetryCause = null;
@@ -1647,11 +1890,29 @@ export class Orchestrator extends TypedEventEmitter {
1647
1890
  const isCurrentAttempt = () => this.abortController === abortController;
1648
1891
  this.abortController = abortController;
1649
1892
  this.currentAttemptCompletionState = completionState;
1893
+ const sessionSource = options?.source ?? 'local';
1650
1894
  this.currentStreamTurn = {
1651
1895
  turnId,
1652
1896
  attempt: attemptNumber,
1653
1897
  conversationId: convId,
1898
+ sessionId: turnId,
1899
+ source: sessionSource,
1654
1900
  };
1901
+ const priorSession = this.currentQuerySession;
1902
+ this.currentQuerySession = this.createQuerySession(convId, turnId, attemptNumber, sessionSource, latestUserMessage, this.streamNudgeCount, completionRetryCount, priorSession);
1903
+ this.saveCurrentQuerySession();
1904
+ if (attemptNumber === 1 && sessionSource === 'channel' && options?.ingress) {
1905
+ this.recordSessionEvent(convId, turnId, 'channel_ingress', `Received channel message from ${options.ingress.senderName || options.ingress.senderId || 'unknown sender'}.`, {
1906
+ channelId: options.ingress.channelId,
1907
+ routeTo: options.ingress.routeTo,
1908
+ senderId: options.ingress.senderId,
1909
+ senderName: options.ingress.senderName,
1910
+ sessionId: options.ingress.sessionId,
1911
+ threadId: options.ingress.threadId,
1912
+ replyToId: options.ingress.replyToId,
1913
+ timestamp: options.ingress.timestamp,
1914
+ });
1915
+ }
1655
1916
  this.currentPartialContent = '';
1656
1917
  this.currentLastContentKind = 'empty';
1657
1918
  this.currentJournaledContentLength = 0;
@@ -1670,7 +1931,13 @@ export class Orchestrator extends TypedEventEmitter {
1670
1931
  stallRetryCount: this.streamNudgeCount,
1671
1932
  completionRetryCount,
1672
1933
  });
1673
- this.emit({ type: 'message:cerebrum:start', conversationId: convId });
1934
+ this.emit({
1935
+ type: 'message:cerebrum:start',
1936
+ conversationId: convId,
1937
+ turnId,
1938
+ sessionId: turnId,
1939
+ source: sessionSource,
1940
+ });
1674
1941
  this.startStreamWatchdog(latestUserMessage);
1675
1942
  let messages = this.conversations.getMessages(convId);
1676
1943
  // On retry: exclude failed attempts' messages from history.
@@ -1774,6 +2041,7 @@ export class Orchestrator extends TypedEventEmitter {
1774
2041
  args: toolCall.args,
1775
2042
  });
1776
2043
  const isInternalTaskSignal = this.isInternalTaskSignalTool(normalizedToolName);
2044
+ const toolIngress = isInternalTaskSignal ? options?.ingress : undefined;
1777
2045
  if (isInternalTaskSignal) {
1778
2046
  completionState.internalToolCallCount++;
1779
2047
  }
@@ -1795,8 +2063,11 @@ export class Orchestrator extends TypedEventEmitter {
1795
2063
  toolCall,
1796
2064
  tools: allTools,
1797
2065
  conversationId: convId,
1798
- sessionKey: 'agent:main',
2066
+ sessionKey: turnId,
1799
2067
  scopeKey: convId,
2068
+ turnId,
2069
+ attempt: attemptNumber,
2070
+ ingress: toolIngress,
1800
2071
  abortSignal: abortController.signal,
1801
2072
  });
1802
2073
  this.logStreamDebug('tool_callback_finished', this.buildToolDebugPayload(toolCall, result, toolName));
@@ -1839,6 +2110,16 @@ export class Orchestrator extends TypedEventEmitter {
1839
2110
  result.output += `\n[Cerebellum warning: ${failedChecks}]`;
1840
2111
  }
1841
2112
  if (verification) {
2113
+ result.metadata = {
2114
+ ...(result.metadata ?? {}),
2115
+ verification: {
2116
+ passed: verification.passed,
2117
+ modelVerdict: verification.modelVerdict,
2118
+ failedChecks: verification.checks
2119
+ .filter((check) => !check.passed)
2120
+ .map((check) => check.description),
2121
+ },
2122
+ };
1842
2123
  const vResult = {
1843
2124
  passed: verification.passed,
1844
2125
  checks: verification.checks,
@@ -1846,7 +2127,12 @@ export class Orchestrator extends TypedEventEmitter {
1846
2127
  toolCallId: toolCall.id,
1847
2128
  toolName,
1848
2129
  };
1849
- this.emit({ type: 'verification:end', result: vResult });
2130
+ this.emit({
2131
+ type: 'verification:end',
2132
+ result: vResult,
2133
+ conversationId: convId,
2134
+ sessionId: turnId,
2135
+ });
1850
2136
  }
1851
2137
  }
1852
2138
  catch {
@@ -1965,7 +2251,14 @@ export class Orchestrator extends TypedEventEmitter {
1965
2251
  }
1966
2252
  }
1967
2253
  const cerebrumMessage = this.conversations.appendMessage(convId, 'cerebrum', displayContent, visibleToolCalls?.length ? { toolCalls: visibleToolCalls } : undefined);
1968
- this.emit({ type: 'message:cerebrum:end', message: cerebrumMessage });
2254
+ this.emit({
2255
+ type: 'message:cerebrum:end',
2256
+ conversationId: convId,
2257
+ turnId,
2258
+ sessionId: turnId,
2259
+ source: sessionSource,
2260
+ message: cerebrumMessage,
2261
+ });
1969
2262
  log.info('stream_finished', {
1970
2263
  turnId,
1971
2264
  attempt: attemptNumber,
@@ -2049,6 +2342,12 @@ export class Orchestrator extends TypedEventEmitter {
2049
2342
  const diagnosticMessage = this.conversations.appendMessage(convId, 'system', assessment.operatorMessage);
2050
2343
  this.emit({ type: 'message:system', message: diagnosticMessage });
2051
2344
  this.emitCompletionTrace('retry_failed', assessment.diagnosis, completionSignal, 'error');
2345
+ this.appendTurnJournalEntry('turn_error', assessment.diagnosis || 'Recovery returned stop.', {
2346
+ retryCause: 'completion',
2347
+ completionRetryCount,
2348
+ stallRetryCount: this.streamNudgeCount,
2349
+ error: assessment.diagnosis || 'Recovery returned stop.',
2350
+ });
2052
2351
  this.emit({
2053
2352
  type: 'error',
2054
2353
  error: new Error(assessment.diagnosis ||
@@ -2076,6 +2375,12 @@ export class Orchestrator extends TypedEventEmitter {
2076
2375
  : '[System fallback] The turn ended repeatedly without a valid completion signal or final answer.');
2077
2376
  this.emit({ type: 'message:system', message: diagnosticMessage });
2078
2377
  this.emitCompletionTrace('retry_failed', `Completion retries exhausted after ${completionRetryCount}/${this.maxCompletionRetries}: ${assessment.diagnosis || completionFailure.message}`, completionSignal, 'error');
2378
+ this.appendTurnJournalEntry('turn_error', `Completion retries exhausted (${completionRetryCount}/${this.maxCompletionRetries}).`, {
2379
+ retryCause: 'completion',
2380
+ completionRetryCount,
2381
+ stallRetryCount: this.streamNudgeCount,
2382
+ error: assessment.diagnosis || 'Completion retries exhausted.',
2383
+ });
2079
2384
  this.emit({
2080
2385
  type: 'error',
2081
2386
  error: new Error(assessment.diagnosis ||
@@ -2118,6 +2423,12 @@ export class Orchestrator extends TypedEventEmitter {
2118
2423
  stallRecovery.assessment.action === 'stop') {
2119
2424
  const systemMessage = this.conversations.appendMessage(convId, 'system', stallRecovery.assessment.operatorMessage);
2120
2425
  this.emit({ type: 'message:system', message: systemMessage });
2426
+ this.appendTurnJournalEntry('turn_error', stallRecovery.assessment.diagnosis || 'Stall recovery returned stop.', {
2427
+ retryCause: 'stall',
2428
+ stallRetryCount: this.streamNudgeCount,
2429
+ completionRetryCount,
2430
+ error: stallRecovery.assessment.diagnosis || 'Stall recovery returned stop.',
2431
+ });
2121
2432
  this.emit({ type: 'error', error: new Error(stallRecovery.assessment.diagnosis) });
2122
2433
  if (failedAttemptMessageIds.length > 0) {
2123
2434
  this.conversations.deleteMessages(convId, failedAttemptMessageIds);
@@ -2129,6 +2440,13 @@ export class Orchestrator extends TypedEventEmitter {
2129
2440
  if (this.cerebellum && !this.cerebellum.isConnected() && abortController.signal.aborted) {
2130
2441
  const err = new Error('Cerebellum disconnected during active response. Restart it with: docker compose up -d cerebellum');
2131
2442
  log.error('Cerebellum disconnected mid-stream', { error: err.message });
2443
+ this.appendTurnJournalEntry('turn_error', err.message, {
2444
+ retryCause,
2445
+ stallRetryCount: this.streamNudgeCount,
2446
+ completionRetryCount,
2447
+ error: err.message,
2448
+ aborted: true,
2449
+ });
2132
2450
  this.emit({ type: 'error', error: err });
2133
2451
  if (failedAttemptMessageIds.length > 0) {
2134
2452
  this.conversations.deleteMessages(convId, failedAttemptMessageIds);
@@ -2191,6 +2509,7 @@ export class Orchestrator extends TypedEventEmitter {
2191
2509
  }
2192
2510
  }
2193
2511
  finally {
2512
+ this.currentQuerySession = null;
2194
2513
  this.currentStreamTurn = null;
2195
2514
  }
2196
2515
  }