@roj-ai/shared 0.0.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.
Files changed (72) hide show
  1. package/dist/chat-protocol.d.ts +60 -0
  2. package/dist/chat-protocol.d.ts.map +1 -0
  3. package/dist/index.d.ts +13 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/lib/domain-utils.d.ts +16 -0
  6. package/dist/lib/domain-utils.d.ts.map +1 -0
  7. package/dist/lib/ids.d.ts +18 -0
  8. package/dist/lib/ids.d.ts.map +1 -0
  9. package/dist/lib/result.d.ts +26 -0
  10. package/dist/lib/result.d.ts.map +1 -0
  11. package/dist/projections/agent-detail-projection.d.ts +91 -0
  12. package/dist/projections/agent-detail-projection.d.ts.map +1 -0
  13. package/dist/projections/agent-registry.d.ts +16 -0
  14. package/dist/projections/agent-registry.d.ts.map +1 -0
  15. package/dist/projections/agent-tree-projection.d.ts +30 -0
  16. package/dist/projections/agent-tree-projection.d.ts.map +1 -0
  17. package/dist/projections/chat-debug.d.ts +34 -0
  18. package/dist/projections/chat-debug.d.ts.map +1 -0
  19. package/dist/projections/events.d.ts +9 -0
  20. package/dist/projections/events.d.ts.map +1 -0
  21. package/dist/projections/index.d.ts +22 -0
  22. package/dist/projections/index.d.ts.map +1 -0
  23. package/dist/projections/mailbox.d.ts +21 -0
  24. package/dist/projections/mailbox.d.ts.map +1 -0
  25. package/dist/projections/metrics.d.ts +30 -0
  26. package/dist/projections/metrics.d.ts.map +1 -0
  27. package/dist/projections/protocol-status.d.ts +9 -0
  28. package/dist/projections/protocol-status.d.ts.map +1 -0
  29. package/dist/projections/services-projection.d.ts +24 -0
  30. package/dist/projections/services-projection.d.ts.map +1 -0
  31. package/dist/projections/session-info.d.ts +19 -0
  32. package/dist/projections/session-info.d.ts.map +1 -0
  33. package/dist/projections/timeline.d.ts +26 -0
  34. package/dist/projections/timeline.d.ts.map +1 -0
  35. package/dist/projections/types.d.ts +182 -0
  36. package/dist/projections/types.d.ts.map +1 -0
  37. package/dist/rpc/client.d.ts +79 -0
  38. package/dist/rpc/client.d.ts.map +1 -0
  39. package/dist/rpc/index.d.ts +9 -0
  40. package/dist/rpc/index.d.ts.map +1 -0
  41. package/dist/src/api-types.d.ts +8 -0
  42. package/dist/src/index.d.ts +17 -0
  43. package/dist/src/rpc/admin-methods.d.ts +99 -0
  44. package/dist/src/rpc/client.d.ts +26 -0
  45. package/dist/src/rpc/definition.d.ts +39 -0
  46. package/dist/src/rpc/instance-methods.d.ts +94 -0
  47. package/dist/src/rpc/methods.d.ts +260 -0
  48. package/dist/src/rpc/server.d.ts +21 -0
  49. package/dist/src/workspace-config.d.ts +16 -0
  50. package/dist/tsconfig.tsbuildinfo +1 -0
  51. package/package.json +36 -0
  52. package/src/chat-protocol.ts +46 -0
  53. package/src/globals.d.ts +3 -0
  54. package/src/index.ts +82 -0
  55. package/src/lib/domain-utils.ts +26 -0
  56. package/src/lib/ids.ts +19 -0
  57. package/src/lib/result.ts +35 -0
  58. package/src/projections/agent-detail-projection.ts +623 -0
  59. package/src/projections/agent-registry.ts +37 -0
  60. package/src/projections/agent-tree-projection.ts +229 -0
  61. package/src/projections/chat-debug.ts +260 -0
  62. package/src/projections/events.ts +10 -0
  63. package/src/projections/index.ts +59 -0
  64. package/src/projections/mailbox.ts +113 -0
  65. package/src/projections/metrics.ts +111 -0
  66. package/src/projections/protocol-status.ts +23 -0
  67. package/src/projections/services-projection.ts +89 -0
  68. package/src/projections/session-info.ts +47 -0
  69. package/src/projections/timeline.ts +228 -0
  70. package/src/projections/types.ts +237 -0
  71. package/src/rpc/client.ts +188 -0
  72. package/src/rpc/index.ts +14 -0
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Agent tree projection - self-contained projection for building agent tree.
3
+ *
4
+ * Replaces the old buildAgentTree() that needed full SessionState.
5
+ * Tracks per-agent: status, parent, pending tool call count, executing flag, mailbox counts.
6
+ */
7
+
8
+ import type { AgentId } from '@roj-ai/sdk'
9
+ import type { ProjectionEvent } from './events.js'
10
+ import { toProtocolStatus } from './protocol-status.js'
11
+ import type { AgentTreeNode } from './types.js'
12
+
13
+ // ============================================================================
14
+ // State
15
+ // ============================================================================
16
+
17
+ interface AgentTreeEntry {
18
+ id: AgentId
19
+ definitionName: string
20
+ status: 'pending' | 'inferring' | 'tool_exec' | 'errored' | 'paused'
21
+ parentId: AgentId | null
22
+ pendingToolCallCount: number
23
+ isExecuting: boolean
24
+ mailboxUnconsumedCount: number
25
+ cost: number
26
+ }
27
+
28
+ export interface AgentTreeProjectionState {
29
+ agents: Map<AgentId, AgentTreeEntry>
30
+ }
31
+
32
+ export function createAgentTreeProjectionState(): AgentTreeProjectionState {
33
+ return { agents: new Map() }
34
+ }
35
+
36
+ // ============================================================================
37
+ // Reducer
38
+ // ============================================================================
39
+
40
+ export function applyEventToAgentTree(state: AgentTreeProjectionState, event: ProjectionEvent): AgentTreeProjectionState {
41
+ switch (event.type) {
42
+ case 'agent_spawned': {
43
+ const newAgents = new Map(state.agents)
44
+ newAgents.set(event.agentId, {
45
+ id: event.agentId,
46
+ definitionName: event.definitionName,
47
+ status: 'pending',
48
+ parentId: event.parentId,
49
+ pendingToolCallCount: 0,
50
+ isExecuting: false,
51
+ mailboxUnconsumedCount: 0,
52
+ cost: 0,
53
+ })
54
+ return { ...state, agents: newAgents }
55
+ }
56
+
57
+ case 'agent_state_changed': {
58
+ const agent = state.agents.get(event.agentId)
59
+ if (!agent) return state
60
+ const newAgents = new Map(state.agents)
61
+ newAgents.set(event.agentId, { ...agent, status: event.toState })
62
+ return { ...state, agents: newAgents }
63
+ }
64
+
65
+ case 'inference_started': {
66
+ const agent = state.agents.get(event.agentId)
67
+ if (!agent) return state
68
+ const newAgents = new Map(state.agents)
69
+ newAgents.set(event.agentId, { ...agent, status: 'inferring' })
70
+ return { ...state, agents: newAgents }
71
+ }
72
+
73
+ case 'inference_completed': {
74
+ const agent = state.agents.get(event.agentId)
75
+ if (!agent) return state
76
+ const toolCallCount = event.response.toolCalls.length
77
+ const newAgents = new Map(state.agents)
78
+
79
+ // Decrement mailbox unconsumed count for consumed messages
80
+ let agents = newAgents
81
+ if (event.consumedMessageIds.length > 0) {
82
+ agents = decrementMailboxCounts(agents, event.agentId, event.consumedMessageIds.length)
83
+ }
84
+
85
+ agents.set(event.agentId, {
86
+ ...agent,
87
+ status: toolCallCount > 0 ? 'tool_exec' : 'pending',
88
+ pendingToolCallCount: toolCallCount,
89
+ isExecuting: false,
90
+ cost: agent.cost + (event.metrics.cost ?? 0),
91
+ })
92
+
93
+ return { ...state, agents }
94
+ }
95
+
96
+ case 'inference_failed': {
97
+ const agent = state.agents.get(event.agentId)
98
+ if (!agent) return state
99
+ const newAgents = new Map(state.agents)
100
+ newAgents.set(event.agentId, { ...agent, status: 'errored' })
101
+ return { ...state, agents: newAgents }
102
+ }
103
+
104
+ case 'tool_started': {
105
+ const agent = state.agents.get(event.agentId)
106
+ if (!agent) return state
107
+ const newAgents = new Map(state.agents)
108
+ newAgents.set(event.agentId, { ...agent, isExecuting: true })
109
+ return { ...state, agents: newAgents }
110
+ }
111
+
112
+ case 'tool_completed':
113
+ case 'tool_failed': {
114
+ const agent = state.agents.get(event.agentId)
115
+ if (!agent) return state
116
+ const remaining = Math.max(0, agent.pendingToolCallCount - 1)
117
+ const newAgents = new Map(state.agents)
118
+ newAgents.set(event.agentId, {
119
+ ...agent,
120
+ pendingToolCallCount: remaining,
121
+ isExecuting: false,
122
+ status: remaining === 0 ? 'pending' : 'tool_exec',
123
+ })
124
+ return { ...state, agents: newAgents }
125
+ }
126
+
127
+ case 'agent_paused': {
128
+ const agent = state.agents.get(event.agentId)
129
+ if (!agent) return state
130
+ const newAgents = new Map(state.agents)
131
+ newAgents.set(event.agentId, { ...agent, status: 'paused' })
132
+ return { ...state, agents: newAgents }
133
+ }
134
+
135
+ case 'agent_resumed': {
136
+ const agent = state.agents.get(event.agentId)
137
+ if (!agent) return state
138
+ const newAgents = new Map(state.agents)
139
+ newAgents.set(event.agentId, { ...agent, status: 'pending' })
140
+ return { ...state, agents: newAgents }
141
+ }
142
+
143
+ case 'session_restarted': {
144
+ const newAgents = new Map(state.agents)
145
+ for (const [agentId, agent] of state.agents) {
146
+ let updated = agent
147
+ let changed = false
148
+
149
+ if (agent.status === 'inferring') {
150
+ updated = { ...updated, status: 'pending' as const }
151
+ changed = true
152
+ }
153
+ if (agent.isExecuting) {
154
+ updated = { ...updated, isExecuting: false }
155
+ changed = true
156
+ }
157
+
158
+ if (changed) {
159
+ newAgents.set(agentId, updated)
160
+ }
161
+ }
162
+ return { ...state, agents: newAgents }
163
+ }
164
+
165
+ case 'mailbox_message': {
166
+ const agent = state.agents.get(event.toAgentId)
167
+ if (!agent) return state
168
+ const newAgents = new Map(state.agents)
169
+ newAgents.set(event.toAgentId, {
170
+ ...agent,
171
+ mailboxUnconsumedCount: agent.mailboxUnconsumedCount + 1,
172
+ })
173
+ return { ...state, agents: newAgents }
174
+ }
175
+
176
+ case 'mailbox_consumed': {
177
+ const agent = state.agents.get(event.agentId)
178
+ if (!agent) return state
179
+ return {
180
+ ...state,
181
+ agents: decrementMailboxCounts(new Map(state.agents), event.agentId, event.messageIds.length),
182
+ }
183
+ }
184
+
185
+ default:
186
+ return state
187
+ }
188
+ }
189
+
190
+ function decrementMailboxCounts(agents: Map<AgentId, AgentTreeEntry>, agentId: AgentId, count: number): Map<AgentId, AgentTreeEntry> {
191
+ const agent = agents.get(agentId)
192
+ if (!agent) return agents
193
+ agents.set(agentId, {
194
+ ...agent,
195
+ mailboxUnconsumedCount: Math.max(0, agent.mailboxUnconsumedCount - count),
196
+ })
197
+ return agents
198
+ }
199
+
200
+ // ============================================================================
201
+ // Query
202
+ // ============================================================================
203
+
204
+ /**
205
+ * Build agent tree nodes from projection state.
206
+ */
207
+ export function buildAgentTreeFromProjection(state: AgentTreeProjectionState): AgentTreeNode[] {
208
+ const allAgents = Array.from(state.agents.values())
209
+ const rootAgents = allAgents.filter((a) => a.parentId === null)
210
+ return rootAgents.map((agent) => buildTreeNode(state, agent))
211
+ }
212
+
213
+ function buildTreeNode(state: AgentTreeProjectionState, agent: AgentTreeEntry): AgentTreeNode {
214
+ const children = Array.from(state.agents.values())
215
+ .filter((a) => a.parentId === agent.id)
216
+ .map((child) => buildTreeNode(state, child))
217
+
218
+ return {
219
+ id: agent.id,
220
+ definitionName: agent.definitionName,
221
+ status: toProtocolStatus(agent.status),
222
+ parentId: agent.parentId,
223
+ children,
224
+ mailboxCount: agent.mailboxUnconsumedCount,
225
+ pendingToolCalls: agent.pendingToolCallCount,
226
+ isExecuting: agent.isExecuting,
227
+ cost: agent.cost,
228
+ }
229
+ }
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Chat debug projection - tracks user-visible chat messages with debug links.
3
+ */
4
+
5
+ import type { AgentId, LLMCallId, ToolCallId } from '@roj-ai/sdk'
6
+ import type { AgentRegistryState } from './agent-registry.js'
7
+ import type { ProjectionEvent } from './events.js'
8
+ import type { DebugChatMessage } from './types.js'
9
+
10
+ /**
11
+ * State for tracking chat messages with debug info.
12
+ */
13
+ export interface ChatDebugState {
14
+ messages: DebugChatMessage[]
15
+ /** Tracking pending tool calls for linking (toolCallId -> { agentId, llmCallId, toolName }) */
16
+ pendingToolCalls: Map<ToolCallId, { agentId: AgentId; llmCallId?: LLMCallId; toolName: string }>
17
+ /** Last LLM call ID by agent (for linking tool calls to inference) */
18
+ lastLLMCallByAgent: Map<AgentId, LLMCallId>
19
+ /** Event index counter */
20
+ eventIndex: number
21
+ }
22
+
23
+ export function createChatDebugState(): ChatDebugState {
24
+ return {
25
+ messages: [],
26
+ pendingToolCalls: new Map(),
27
+ lastLLMCallByAgent: new Map(),
28
+ eventIndex: 0,
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Apply event to chat debug state.
34
+ * @param registry Agent registry for name lookups
35
+ */
36
+ export function applyEventToChatDebug(
37
+ state: ChatDebugState,
38
+ event: ProjectionEvent,
39
+ registry: AgentRegistryState,
40
+ ): ChatDebugState {
41
+ const currentIndex = state.eventIndex
42
+ const nextIndex = currentIndex + 1
43
+
44
+ const getAgentName = (agentId: AgentId): string => {
45
+ return registry.names.get(agentId) ?? 'unknown'
46
+ }
47
+
48
+ switch (event.type) {
49
+ case 'inference_completed': {
50
+ // Track the last LLM call for this agent
51
+ if (event.llmCallId) {
52
+ const newLastLLMCallByAgent = new Map(state.lastLLMCallByAgent)
53
+ newLastLLMCallByAgent.set(event.agentId, event.llmCallId)
54
+ return {
55
+ ...state,
56
+ lastLLMCallByAgent: newLastLLMCallByAgent,
57
+ eventIndex: nextIndex,
58
+ }
59
+ }
60
+ return { ...state, eventIndex: nextIndex }
61
+ }
62
+
63
+ case 'tool_started': {
64
+ // Only track send_user_message and ask_user tools
65
+ if (event.toolName === 'send_user_message' || event.toolName === 'ask_user') {
66
+ const llmCallId = state.lastLLMCallByAgent.get(event.agentId)
67
+ const newPendingToolCalls = new Map(state.pendingToolCalls)
68
+ newPendingToolCalls.set(event.toolCallId, {
69
+ agentId: event.agentId,
70
+ llmCallId,
71
+ toolName: event.toolName,
72
+ })
73
+ return {
74
+ ...state,
75
+ pendingToolCalls: newPendingToolCalls,
76
+ eventIndex: nextIndex,
77
+ }
78
+ }
79
+ return { ...state, eventIndex: nextIndex }
80
+ }
81
+
82
+ case 'user_message_sent': {
83
+ // Find the pending tool call for this message
84
+ let toolCallId: ToolCallId | undefined
85
+ let llmCallId: LLMCallId | undefined
86
+ let agentId: AgentId | undefined
87
+
88
+ // Look through pending tool calls to find a send_user_message from the same agent
89
+ for (const [tcId, info] of state.pendingToolCalls) {
90
+ if (info.toolName === 'send_user_message' && info.agentId === event.agentId) {
91
+ toolCallId = tcId
92
+ llmCallId = info.llmCallId
93
+ agentId = info.agentId
94
+ break
95
+ }
96
+ }
97
+
98
+ // Clean up the used pending tool call
99
+ const newPendingToolCalls = new Map(state.pendingToolCalls)
100
+ if (toolCallId) {
101
+ newPendingToolCalls.delete(toolCallId)
102
+ }
103
+
104
+ const newMessage: DebugChatMessage = {
105
+ type: 'agent_message',
106
+ messageId: event.messageId,
107
+ content: event.message,
108
+ timestamp: event.timestamp,
109
+ eventIndex: currentIndex,
110
+ agentId: agentId ?? event.agentId,
111
+ agentName: getAgentName(agentId ?? event.agentId),
112
+ llmCallId,
113
+ toolCallId,
114
+ format: event.format,
115
+ }
116
+
117
+ return {
118
+ ...state,
119
+ messages: [...state.messages, newMessage],
120
+ pendingToolCalls: newPendingToolCalls,
121
+ eventIndex: nextIndex,
122
+ }
123
+ }
124
+
125
+ case 'user_question_asked': {
126
+ // Find the pending tool call for this question
127
+ let toolCallId: ToolCallId | undefined
128
+ let llmCallId: LLMCallId | undefined
129
+ let agentId: AgentId | undefined
130
+
131
+ // Look through pending tool calls to find an ask_user from the same agent
132
+ for (const [tcId, info] of state.pendingToolCalls) {
133
+ if (info.toolName === 'ask_user' && info.agentId === event.agentId) {
134
+ toolCallId = tcId
135
+ llmCallId = info.llmCallId
136
+ agentId = info.agentId
137
+ break
138
+ }
139
+ }
140
+
141
+ // Clean up the used pending tool call
142
+ const newPendingToolCalls = new Map(state.pendingToolCalls)
143
+ if (toolCallId) {
144
+ newPendingToolCalls.delete(toolCallId)
145
+ }
146
+
147
+ const newMessage: DebugChatMessage = {
148
+ type: 'ask_user',
149
+ messageId: event.messageId,
150
+ content: event.question,
151
+ timestamp: event.timestamp,
152
+ eventIndex: currentIndex,
153
+ agentId: agentId ?? event.agentId,
154
+ agentName: getAgentName(agentId ?? event.agentId),
155
+ llmCallId,
156
+ toolCallId,
157
+ inputType: event.inputType,
158
+ answered: false,
159
+ }
160
+
161
+ return {
162
+ ...state,
163
+ messages: [...state.messages, newMessage],
164
+ pendingToolCalls: newPendingToolCalls,
165
+ eventIndex: nextIndex,
166
+ }
167
+ }
168
+
169
+ case 'user_chat_message_received': {
170
+ const newMessage: DebugChatMessage = {
171
+ type: 'user_message',
172
+ messageId: event.messageId,
173
+ content: event.content,
174
+ timestamp: event.timestamp,
175
+ eventIndex: currentIndex,
176
+ agentId: event.agentId,
177
+ agentName: getAgentName(event.agentId),
178
+ }
179
+
180
+ return {
181
+ ...state,
182
+ messages: [...state.messages, newMessage],
183
+ eventIndex: nextIndex,
184
+ }
185
+ }
186
+
187
+ case 'user_chat_answer_received': {
188
+ const updatedMessages = state.messages.map((msg) => {
189
+ if (msg.type === 'ask_user' && msg.messageId === event.questionId) {
190
+ return {
191
+ ...msg,
192
+ answered: true,
193
+ answer: event.answerValue,
194
+ }
195
+ }
196
+ return msg
197
+ })
198
+
199
+ return {
200
+ ...state,
201
+ messages: updatedMessages,
202
+ eventIndex: nextIndex,
203
+ }
204
+ }
205
+
206
+ case 'mailbox_message': {
207
+ // Only handle user messages (not answers to questions)
208
+ if (event.message.from === 'user' && !event.message.answerTo) {
209
+ const newMessage: DebugChatMessage = {
210
+ type: 'user_message',
211
+ messageId: event.message.id,
212
+ content: event.message.content,
213
+ timestamp: event.message.timestamp,
214
+ eventIndex: currentIndex,
215
+ agentId: event.toAgentId,
216
+ agentName: getAgentName(event.toAgentId),
217
+ mailboxMessageId: event.message.id,
218
+ }
219
+
220
+ return {
221
+ ...state,
222
+ messages: [...state.messages, newMessage],
223
+ eventIndex: nextIndex,
224
+ }
225
+ }
226
+
227
+ // Handle answer to question - update the ask_user message
228
+ if (event.message.from === 'user' && event.message.answerTo) {
229
+ const updatedMessages = state.messages.map((msg) => {
230
+ if (msg.type === 'ask_user' && msg.messageId === event.message.answerTo) {
231
+ return {
232
+ ...msg,
233
+ answered: true,
234
+ answer: event.message.answerValue,
235
+ }
236
+ }
237
+ return msg
238
+ })
239
+
240
+ return {
241
+ ...state,
242
+ messages: updatedMessages,
243
+ eventIndex: nextIndex,
244
+ }
245
+ }
246
+
247
+ return { ...state, eventIndex: nextIndex }
248
+ }
249
+
250
+ default:
251
+ return { ...state, eventIndex: nextIndex }
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Get chat debug messages sorted by timestamp.
257
+ */
258
+ export function getChatDebugMessages(state: ChatDebugState): DebugChatMessage[] {
259
+ return state.messages
260
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * ProjectionEvent — union of all event types handled by shared projections.
3
+ *
4
+ * Boundary code (event-store.ts, debug.ts) casts DomainEvent → ProjectionEvent once.
5
+ * Individual projection reducers accept ProjectionEvent directly — no internal casts.
6
+ */
7
+
8
+ import type { BuiltinEvent, ServiceStatusChangedEvent, SkillLoadedEvent } from '@roj-ai/sdk'
9
+
10
+ export type ProjectionEvent = BuiltinEvent | SkillLoadedEvent | ServiceStatusChangedEvent
@@ -0,0 +1,59 @@
1
+ // Projection event type
2
+ export type { ProjectionEvent } from './events.js'
3
+
4
+ // Types
5
+ export type {
6
+ AgentTreeNode,
7
+ AssistantConversationMessageView,
8
+ ConversationMessageView,
9
+ DebugChatMessage,
10
+ GetAgentDetailResponse,
11
+ GetEventsResponse,
12
+ GetMetricsResponse,
13
+ GlobalMailboxMessage,
14
+ MailboxMessageView,
15
+ SystemConversationMessageView,
16
+ TimelineItem,
17
+ ToolCallView,
18
+ ToolConversationMessageView,
19
+ UserConversationMessageView,
20
+ } from './types.js'
21
+
22
+ // Protocol status
23
+ export { toProtocolStatus } from './protocol-status.js'
24
+
25
+ // Agent registry
26
+ export type { AgentRegistryState } from './agent-registry.js'
27
+ export { applyEventToAgentRegistry, createAgentRegistryState, getAgentName } from './agent-registry.js'
28
+
29
+ // Agent tree projection (self-contained — no SessionState)
30
+ export type { AgentTreeProjectionState } from './agent-tree-projection.js'
31
+ export { applyEventToAgentTree, buildAgentTreeFromProjection, createAgentTreeProjectionState } from './agent-tree-projection.js'
32
+
33
+ // Metrics
34
+ export type { MetricsState } from './metrics.js'
35
+ export { applyEventToMetrics, createMetricsState, metricsStateToResponse } from './metrics.js'
36
+
37
+ // Timeline
38
+ export type { TimelineState } from './timeline.js'
39
+ export { applyEventToTimeline, createTimelineState, getTimelineItems } from './timeline.js'
40
+
41
+ // Mailbox projection
42
+ export type { MailboxState } from './mailbox.js'
43
+ export { applyEventToMailbox, createMailboxState, getMailboxMessages } from './mailbox.js'
44
+
45
+ // Chat debug
46
+ export type { ChatDebugState } from './chat-debug.js'
47
+ export { applyEventToChatDebug, createChatDebugState, getChatDebugMessages } from './chat-debug.js'
48
+
49
+ // Agent detail projection (self-contained — no SessionState)
50
+ export type { AgentDetailProjectionState } from './agent-detail-projection.js'
51
+ export { applyEventToAgentDetail, createAgentDetailProjectionState, getAgentDetail } from './agent-detail-projection.js'
52
+
53
+ // Session info projection
54
+ export type { SessionInfoState } from './session-info.js'
55
+ export { applyEventToSessionInfo, createSessionInfoState } from './session-info.js'
56
+
57
+ // Services projection
58
+ export type { ServiceEntry, ServicesProjectionState, ServiceStatus } from './services-projection.js'
59
+ export { applyEventToServices, createServicesProjectionState } from './services-projection.js'
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Global mailbox projection - tracks all inter-agent messages.
3
+ */
4
+
5
+ import type { AgentId } from '@roj-ai/sdk'
6
+ import type { AgentRegistryState } from './agent-registry.js'
7
+ import type { ProjectionEvent } from './events.js'
8
+ import type { GlobalMailboxMessage } from './types.js'
9
+
10
+ export interface MailboxState {
11
+ messages: GlobalMailboxMessage[]
12
+ consumedIds: Set<string>
13
+ }
14
+
15
+ export function createMailboxState(): MailboxState {
16
+ return {
17
+ messages: [],
18
+ consumedIds: new Set(),
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Apply event to mailbox state.
24
+ * @param registry Agent registry for name lookups
25
+ */
26
+ export function applyEventToMailbox(
27
+ state: MailboxState,
28
+ event: ProjectionEvent,
29
+ registry: AgentRegistryState,
30
+ ): MailboxState {
31
+ switch (event.type) {
32
+ case 'mailbox_message': {
33
+ const msg = event.message
34
+
35
+ // Determine from agent name
36
+ let fromAgentId: string
37
+ let fromAgentName: string
38
+
39
+ if (msg.from === 'user') {
40
+ fromAgentId = 'user'
41
+ fromAgentName = 'User'
42
+ } else if (msg.from === 'orchestrator' || msg.from === 'communicator') {
43
+ fromAgentId = msg.from
44
+ fromAgentName = msg.from.charAt(0).toUpperCase() + msg.from.slice(1)
45
+ } else {
46
+ fromAgentId = msg.from
47
+ fromAgentName = registry.names.get(msg.from as AgentId) ?? 'unknown'
48
+ }
49
+
50
+ const newMessage: GlobalMailboxMessage = {
51
+ id: msg.id,
52
+ fromAgentId,
53
+ fromAgentName,
54
+ toAgentId: event.toAgentId,
55
+ toAgentName: registry.names.get(event.toAgentId) ?? 'unknown',
56
+ content: msg.content,
57
+ timestamp: msg.timestamp,
58
+ consumed: state.consumedIds.has(msg.id),
59
+ }
60
+
61
+ return {
62
+ ...state,
63
+ messages: [...state.messages, newMessage],
64
+ }
65
+ }
66
+
67
+ case 'mailbox_consumed': {
68
+ const newConsumedIds = new Set(state.consumedIds)
69
+ for (const msgId of event.messageIds) {
70
+ newConsumedIds.add(msgId)
71
+ }
72
+
73
+ // Update consumed status in existing messages
74
+ const consumedSet = new Set(event.messageIds as string[])
75
+ const updatedMessages = state.messages.map((m) => consumedSet.has(m.id) ? { ...m, consumed: true } : m)
76
+
77
+ return {
78
+ messages: updatedMessages,
79
+ consumedIds: newConsumedIds,
80
+ }
81
+ }
82
+
83
+ case 'inference_completed': {
84
+ // inference_completed also marks messages as consumed
85
+ if (event.consumedMessageIds.length === 0) return state
86
+
87
+ const newConsumedIds = new Set(state.consumedIds)
88
+ for (const msgId of event.consumedMessageIds) {
89
+ newConsumedIds.add(msgId)
90
+ }
91
+
92
+ // Update consumed status in existing messages
93
+ const consumedSet = new Set(event.consumedMessageIds as string[])
94
+ const updatedMessages = state.messages.map((m) => consumedSet.has(m.id) ? { ...m, consumed: true } : m)
95
+
96
+ return {
97
+ messages: updatedMessages,
98
+ consumedIds: newConsumedIds,
99
+ }
100
+ }
101
+
102
+ default:
103
+ return state
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Get mailbox messages sorted by timestamp.
109
+ */
110
+ export function getMailboxMessages(state: MailboxState): GlobalMailboxMessage[] {
111
+ // Messages are already in order since we append them
112
+ return state.messages
113
+ }