@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,623 @@
1
+ /**
2
+ * Agent detail projection - self-contained projection for agent detail views.
3
+ *
4
+ * Replaces useAgentDetail (client) and buildAgentDetail (CLI) that needed full SessionState.
5
+ * Tracks per-agent: conversation history, tool calls, mailbox, counters, skills.
6
+ */
7
+
8
+ import type { AgentCounters, AgentId, AgentPauseReason, LLMCallId, MessageId, ToolCallId } from '@roj-ai/sdk'
9
+ import { contentToString } from '../lib/domain-utils.js'
10
+ import type { ProjectionEvent } from './events.js'
11
+ import { toProtocolStatus } from './protocol-status.js'
12
+ import type { ConversationMessageView, GetAgentDetailResponse, MailboxMessageView, ToolCallView } from './types.js'
13
+
14
+ // ============================================================================
15
+ // Agent view model (internal per-agent state)
16
+ // ============================================================================
17
+
18
+ interface LLMMessage {
19
+ role: 'user' | 'assistant' | 'tool' | 'system'
20
+ content: string | Array<{ type: string; text?: string }>
21
+ toolCalls?: Array<{ id: ToolCallId; name: string; input: unknown }>
22
+ toolCallId?: ToolCallId
23
+ isError?: boolean
24
+ sourceMessageIds?: MessageId[]
25
+ timestamp?: number
26
+ cost?: number
27
+ llmCallId?: LLMCallId
28
+ promptTokens?: number
29
+ cachedTokens?: number
30
+ cacheWriteTokens?: number
31
+ }
32
+
33
+ interface ToolCall {
34
+ id: ToolCallId
35
+ name: string
36
+ input: unknown
37
+ }
38
+
39
+ interface PendingToolResult {
40
+ toolCallId: ToolCallId
41
+ toolName: string
42
+ timestamp: number
43
+ isError: boolean
44
+ content: string | Array<{ type: string; text?: string }>
45
+ }
46
+
47
+ interface AgentViewModel {
48
+ id: AgentId
49
+ definitionName: string
50
+ status: 'pending' | 'inferring' | 'tool_exec' | 'errored' | 'paused'
51
+ parentId: AgentId | null
52
+ conversationHistory: LLMMessage[]
53
+ pendingToolCalls: ToolCall[]
54
+ executingToolCall?: { toolCallId: ToolCallId; toolName: string; startedAt: number }
55
+ pendingToolResults: PendingToolResult[]
56
+ /** Messages pending addition to history (set by inference_started, committed by inference_completed) */
57
+ pendingMessages: LLMMessage[]
58
+ typedInput?: unknown
59
+ pauseReason?: AgentPauseReason
60
+ pauseMessage?: string
61
+ cost: number
62
+ }
63
+
64
+ interface MailboxEntry {
65
+ id: MessageId
66
+ from: string
67
+ content: string
68
+ timestamp: number
69
+ consumed: boolean
70
+ }
71
+
72
+ interface LoadedSkillEntry {
73
+ id: string
74
+ name: string
75
+ loadedAt: number
76
+ }
77
+
78
+ // ============================================================================
79
+ // State
80
+ // ============================================================================
81
+
82
+ export interface AgentDetailProjectionState {
83
+ agents: Map<AgentId, AgentViewModel>
84
+ agentMailboxes: Map<AgentId, MailboxEntry[]>
85
+ agentCounters: Map<AgentId, AgentCounters>
86
+ agentSkills: Map<AgentId, LoadedSkillEntry[]>
87
+ }
88
+
89
+ export function createAgentDetailProjectionState(): AgentDetailProjectionState {
90
+ return {
91
+ agents: new Map(),
92
+ agentMailboxes: new Map(),
93
+ agentCounters: new Map(),
94
+ agentSkills: new Map(),
95
+ }
96
+ }
97
+
98
+ const createDefaultCounters = (): AgentCounters => ({
99
+ inferenceCount: 0,
100
+ toolCallCount: 0,
101
+ spawnedAgentCount: 0,
102
+ messagesSentCount: 0,
103
+ consecutiveToolFailures: {},
104
+ recentToolCallHashes: [],
105
+ recentResponseHashes: [],
106
+ })
107
+
108
+ // ============================================================================
109
+ // Reducer
110
+ // ============================================================================
111
+
112
+ export function applyEventToAgentDetail(state: AgentDetailProjectionState, event: ProjectionEvent): AgentDetailProjectionState {
113
+ switch (event.type) {
114
+ // ---- Core agent lifecycle ----
115
+
116
+ case 'agent_spawned': {
117
+ const newAgents = new Map(state.agents)
118
+ newAgents.set(event.agentId, {
119
+ id: event.agentId,
120
+ definitionName: event.definitionName,
121
+ parentId: event.parentId,
122
+ status: 'pending',
123
+ conversationHistory: [],
124
+ pendingToolCalls: [],
125
+ pendingToolResults: [],
126
+ pendingMessages: [],
127
+ typedInput: event.typedInput,
128
+ cost: 0,
129
+ })
130
+
131
+ const newCounters = new Map(state.agentCounters)
132
+ newCounters.set(event.agentId, createDefaultCounters())
133
+
134
+ // Increment parent's spawnedAgentCount
135
+ if (event.parentId) {
136
+ const parentCounters = newCounters.get(event.parentId)
137
+ if (parentCounters) {
138
+ newCounters.set(event.parentId, {
139
+ ...parentCounters,
140
+ spawnedAgentCount: parentCounters.spawnedAgentCount + 1,
141
+ })
142
+ }
143
+ }
144
+
145
+ return { ...state, agents: newAgents, agentCounters: newCounters }
146
+ }
147
+
148
+ case 'agent_state_changed': {
149
+ const agent = state.agents.get(event.agentId)
150
+ if (!agent) return state
151
+ const newAgents = new Map(state.agents)
152
+ newAgents.set(event.agentId, { ...agent, status: event.toState })
153
+ return { ...state, agents: newAgents }
154
+ }
155
+
156
+ case 'inference_started': {
157
+ const agent = state.agents.get(event.agentId)
158
+ if (!agent) return state
159
+ const newAgents = new Map(state.agents)
160
+ newAgents.set(event.agentId, {
161
+ ...agent,
162
+ status: 'inferring',
163
+ pendingMessages: event.messages.map((m) => ({ ...m, timestamp: event.timestamp })),
164
+ })
165
+ return { ...state, agents: newAgents }
166
+ }
167
+
168
+ case 'inference_completed': {
169
+ const agent = state.agents.get(event.agentId)
170
+ if (!agent) return state
171
+
172
+ const toolCalls: ToolCall[] = event.response.toolCalls.map((tc) => ({
173
+ id: tc.id,
174
+ name: tc.name,
175
+ input: tc.input,
176
+ }))
177
+
178
+ const assistantMessage: LLMMessage = {
179
+ role: 'assistant',
180
+ content: event.response.content ?? '',
181
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
182
+ timestamp: event.timestamp,
183
+ cost: event.metrics.cost ?? undefined,
184
+ llmCallId: event.llmCallId ?? undefined,
185
+ promptTokens: event.metrics.promptTokens,
186
+ cachedTokens: event.metrics.cachedTokens ?? undefined,
187
+ cacheWriteTokens: event.metrics.cacheWriteTokens ?? undefined,
188
+ }
189
+
190
+ const hasToolCalls = toolCalls.length > 0
191
+
192
+ const newAgents = new Map(state.agents)
193
+ newAgents.set(event.agentId, {
194
+ ...agent,
195
+ status: hasToolCalls ? 'tool_exec' : 'pending',
196
+ conversationHistory: [...agent.conversationHistory, ...agent.pendingMessages, assistantMessage],
197
+ pendingToolCalls: toolCalls,
198
+ pendingMessages: [],
199
+ pendingToolResults: [],
200
+ cost: agent.cost + (event.metrics.cost ?? 0),
201
+ })
202
+
203
+ // Update counters
204
+ const counters = state.agentCounters.get(event.agentId)
205
+ if (counters) {
206
+ const newCounters = new Map(state.agentCounters)
207
+ newCounters.set(event.agentId, {
208
+ ...counters,
209
+ inferenceCount: counters.inferenceCount + 1,
210
+ })
211
+ return { ...state, agents: newAgents, agentCounters: newCounters }
212
+ }
213
+
214
+ return { ...state, agents: newAgents }
215
+ }
216
+
217
+ case 'inference_failed': {
218
+ const agent = state.agents.get(event.agentId)
219
+ if (!agent) return state
220
+ const newAgents = new Map(state.agents)
221
+ newAgents.set(event.agentId, {
222
+ ...agent,
223
+ status: 'errored',
224
+ pendingMessages: [],
225
+ })
226
+ return { ...state, agents: newAgents }
227
+ }
228
+
229
+ case 'tool_started': {
230
+ const agent = state.agents.get(event.agentId)
231
+ if (!agent) return state
232
+ const newAgents = new Map(state.agents)
233
+ newAgents.set(event.agentId, {
234
+ ...agent,
235
+ executingToolCall: {
236
+ toolCallId: event.toolCallId,
237
+ toolName: event.toolName,
238
+ startedAt: event.timestamp,
239
+ },
240
+ })
241
+
242
+ // Update counters
243
+ const counters = state.agentCounters.get(event.agentId)
244
+ if (counters) {
245
+ const newCounters = new Map(state.agentCounters)
246
+ newCounters.set(event.agentId, {
247
+ ...counters,
248
+ toolCallCount: counters.toolCallCount + 1,
249
+ })
250
+ return { ...state, agents: newAgents, agentCounters: newCounters }
251
+ }
252
+
253
+ return { ...state, agents: newAgents }
254
+ }
255
+
256
+ case 'tool_completed': {
257
+ const agent = state.agents.get(event.agentId)
258
+ if (!agent) return state
259
+ const remaining = agent.pendingToolCalls.filter((tc) => tc.id !== event.toolCallId)
260
+ const toolName = agent.executingToolCall?.toolName
261
+ ?? agent.pendingToolCalls.find((tc) => tc.id === event.toolCallId)?.name
262
+ ?? 'unknown'
263
+
264
+ const pendingToolResult: PendingToolResult = {
265
+ toolCallId: event.toolCallId,
266
+ toolName,
267
+ timestamp: event.timestamp,
268
+ isError: false,
269
+ content: event.result,
270
+ }
271
+
272
+ const newAgents = new Map(state.agents)
273
+ newAgents.set(event.agentId, {
274
+ ...agent,
275
+ pendingToolCalls: remaining,
276
+ pendingToolResults: [...agent.pendingToolResults, pendingToolResult],
277
+ status: remaining.length === 0 ? 'pending' : 'tool_exec',
278
+ executingToolCall: undefined,
279
+ })
280
+
281
+ // Reset consecutive failures for this tool
282
+ const counters = state.agentCounters.get(event.agentId)
283
+ if (counters) {
284
+ const { [toolName]: _, ...restFailures } = counters.consecutiveToolFailures
285
+ const newCounters = new Map(state.agentCounters)
286
+ newCounters.set(event.agentId, {
287
+ ...counters,
288
+ consecutiveToolFailures: restFailures,
289
+ })
290
+ return { ...state, agents: newAgents, agentCounters: newCounters }
291
+ }
292
+
293
+ return { ...state, agents: newAgents }
294
+ }
295
+
296
+ case 'tool_failed': {
297
+ const agent = state.agents.get(event.agentId)
298
+ if (!agent) return state
299
+ const remaining = agent.pendingToolCalls.filter((tc) => tc.id !== event.toolCallId)
300
+ const toolName = agent.executingToolCall?.toolName
301
+ ?? agent.pendingToolCalls.find((tc) => tc.id === event.toolCallId)?.name
302
+ ?? 'unknown'
303
+
304
+ const pendingToolResult: PendingToolResult = {
305
+ toolCallId: event.toolCallId,
306
+ toolName,
307
+ timestamp: event.timestamp,
308
+ isError: true,
309
+ content: event.error,
310
+ }
311
+
312
+ const newAgents = new Map(state.agents)
313
+ newAgents.set(event.agentId, {
314
+ ...agent,
315
+ pendingToolCalls: remaining,
316
+ pendingToolResults: [...agent.pendingToolResults, pendingToolResult],
317
+ status: remaining.length === 0 ? 'pending' : 'tool_exec',
318
+ executingToolCall: undefined,
319
+ })
320
+
321
+ // Increment consecutive failures for this tool
322
+ const counters = state.agentCounters.get(event.agentId)
323
+ if (counters) {
324
+ const currentEntry = counters.consecutiveToolFailures[toolName]
325
+ const newCounters = new Map(state.agentCounters)
326
+ newCounters.set(event.agentId, {
327
+ ...counters,
328
+ consecutiveToolFailures: {
329
+ ...counters.consecutiveToolFailures,
330
+ [toolName]: { count: (currentEntry?.count ?? 0) + 1, lastError: event.error },
331
+ },
332
+ })
333
+ return { ...state, agents: newAgents, agentCounters: newCounters }
334
+ }
335
+
336
+ return { ...state, agents: newAgents }
337
+ }
338
+
339
+ case 'context_compacted': {
340
+ const agent = state.agents.get(event.agentId)
341
+ if (!agent) return state
342
+ const newAgents = new Map(state.agents)
343
+ newAgents.set(event.agentId, {
344
+ ...agent,
345
+ conversationHistory: event.newConversationHistory.map((m): LLMMessage => ({
346
+ role: m.role,
347
+ content: m.content,
348
+ })),
349
+ })
350
+ return { ...state, agents: newAgents }
351
+ }
352
+
353
+ case 'agent_paused': {
354
+ const agent = state.agents.get(event.agentId)
355
+ if (!agent) return state
356
+ const newAgents = new Map(state.agents)
357
+ newAgents.set(event.agentId, {
358
+ ...agent,
359
+ status: 'paused',
360
+ pauseReason: event.reason,
361
+ pauseMessage: event.message,
362
+ })
363
+ return { ...state, agents: newAgents }
364
+ }
365
+
366
+ case 'agent_resumed': {
367
+ const agent = state.agents.get(event.agentId)
368
+ if (!agent) return state
369
+ const newAgents = new Map(state.agents)
370
+ newAgents.set(event.agentId, {
371
+ ...agent,
372
+ status: 'pending',
373
+ pauseReason: undefined,
374
+ pauseMessage: undefined,
375
+ })
376
+
377
+ // Reset counters on resume
378
+ const counters = state.agentCounters.get(event.agentId)
379
+ if (counters) {
380
+ const newCounters = new Map(state.agentCounters)
381
+ newCounters.set(event.agentId, {
382
+ ...counters,
383
+ inferenceCount: 0,
384
+ toolCallCount: 0,
385
+ spawnedAgentCount: 0,
386
+ messagesSentCount: 0,
387
+ consecutiveToolFailures: {},
388
+ recentToolCallHashes: [],
389
+ recentResponseHashes: [],
390
+ })
391
+ return { ...state, agents: newAgents, agentCounters: newCounters }
392
+ }
393
+
394
+ return { ...state, agents: newAgents }
395
+ }
396
+
397
+ case 'session_restarted': {
398
+ const newAgents = new Map(state.agents)
399
+ for (const [agentId, agent] of state.agents) {
400
+ let updated = agent
401
+ let changed = false
402
+
403
+ if (agent.status === 'inferring') {
404
+ updated = { ...updated, status: 'pending' as const, pendingMessages: [] }
405
+ changed = true
406
+ }
407
+ if (agent.executingToolCall !== undefined) {
408
+ updated = { ...updated, executingToolCall: undefined }
409
+ changed = true
410
+ }
411
+
412
+ if (changed) {
413
+ newAgents.set(agentId, updated)
414
+ }
415
+ }
416
+ return { ...state, agents: newAgents }
417
+ }
418
+
419
+ case 'agent_conversation_spliced': {
420
+ const agent = state.agents.get(event.agentId)
421
+ if (!agent) return state
422
+ const newHistory = [...agent.conversationHistory]
423
+ newHistory.splice(event.start, event.deleteCount, ...(event.insert ?? []))
424
+ const newAgents = new Map(state.agents)
425
+ newAgents.set(event.agentId, {
426
+ ...agent,
427
+ conversationHistory: newHistory,
428
+ pendingToolCalls: [],
429
+ pendingToolResults: [],
430
+ pendingMessages: [],
431
+ executingToolCall: undefined,
432
+ status: 'pending',
433
+ })
434
+ return { ...state, agents: newAgents }
435
+ }
436
+
437
+ // ---- Mailbox ----
438
+
439
+ case 'mailbox_message': {
440
+ const msg = event.message
441
+ const entry: MailboxEntry = {
442
+ id: msg.id,
443
+ from: msg.from,
444
+ content: msg.content,
445
+ timestamp: msg.timestamp,
446
+ consumed: msg.consumed,
447
+ }
448
+
449
+ const existing = state.agentMailboxes.get(event.toAgentId) ?? []
450
+ const newMailboxes = new Map(state.agentMailboxes)
451
+ newMailboxes.set(event.toAgentId, [...existing, entry])
452
+
453
+ // Increment sender's messagesSentCount if sender is an agent
454
+ const senderAgentId = typeof msg.from === 'string' && msg.from !== 'user'
455
+ && msg.from !== 'orchestrator' && msg.from !== 'communicator'
456
+ ? msg.from
457
+ : null
458
+
459
+ if (senderAgentId) {
460
+ const senderCounters = state.agentCounters.get(senderAgentId as AgentId)
461
+ if (senderCounters) {
462
+ const newCounters = new Map(state.agentCounters)
463
+ newCounters.set(senderAgentId as AgentId, {
464
+ ...senderCounters,
465
+ messagesSentCount: senderCounters.messagesSentCount + 1,
466
+ })
467
+ return { ...state, agentMailboxes: newMailboxes, agentCounters: newCounters }
468
+ }
469
+ }
470
+
471
+ return { ...state, agentMailboxes: newMailboxes }
472
+ }
473
+
474
+ case 'mailbox_consumed': {
475
+ const existing = state.agentMailboxes.get(event.agentId)
476
+ if (!existing) return state
477
+
478
+ const consumedSet = new Set(event.messageIds as string[])
479
+ const updated = existing.map((m) => consumedSet.has(m.id) ? { ...m, consumed: true } : m)
480
+ const newMailboxes = new Map(state.agentMailboxes)
481
+ newMailboxes.set(event.agentId, updated)
482
+ return { ...state, agentMailboxes: newMailboxes }
483
+ }
484
+
485
+ // ---- Skills ----
486
+
487
+ case 'skill_loaded': {
488
+ const agentSkills = state.agentSkills.get(event.agentId) ?? []
489
+ const newSkills = new Map(state.agentSkills)
490
+ newSkills.set(event.agentId, [...agentSkills, {
491
+ id: event.skillId,
492
+ name: event.skillName,
493
+ loadedAt: event.timestamp,
494
+ }])
495
+ return { ...state, agentSkills: newSkills }
496
+ }
497
+
498
+ default:
499
+ return state
500
+ }
501
+ }
502
+
503
+ // ============================================================================
504
+ // Query
505
+ // ============================================================================
506
+
507
+ const truncate = (text: string): string => text.length > 500 ? text.slice(0, 500) + '...' : text
508
+
509
+ /**
510
+ * Get agent detail response from projection state.
511
+ */
512
+ export function getAgentDetail(state: AgentDetailProjectionState, agentId: AgentId): GetAgentDetailResponse | null {
513
+ const agent = state.agents.get(agentId)
514
+ if (!agent) return null
515
+
516
+ // Mailbox
517
+ const agentMailbox = state.agentMailboxes.get(agentId) ?? []
518
+ const mailbox: MailboxMessageView[] = agentMailbox.map((m) => ({
519
+ id: m.id,
520
+ from: m.from,
521
+ content: m.content,
522
+ timestamp: m.timestamp,
523
+ consumed: m.consumed,
524
+ }))
525
+
526
+ // Conversation history
527
+ const conversationHistory: ConversationMessageView[] = agent.conversationHistory.map((m) => {
528
+ switch (m.role) {
529
+ case 'user': {
530
+ const full = contentToString(m.content)
531
+ return { role: 'user' as const, content: truncate(full), fullContent: full, timestamp: m.timestamp }
532
+ }
533
+ case 'assistant': {
534
+ const full = contentToString(m.content)
535
+ return {
536
+ role: 'assistant' as const,
537
+ content: truncate(full),
538
+ fullContent: full,
539
+ toolCalls: m.toolCalls?.map((tc) => ({
540
+ id: tc.id,
541
+ name: tc.name,
542
+ input: tc.input,
543
+ })),
544
+ timestamp: m.timestamp,
545
+ cost: m.cost,
546
+ llmCallId: m.llmCallId,
547
+ promptTokens: m.promptTokens,
548
+ cachedTokens: m.cachedTokens,
549
+ cacheWriteTokens: m.cacheWriteTokens,
550
+ }
551
+ }
552
+ case 'tool': {
553
+ const full = contentToString(m.content)
554
+ return {
555
+ role: 'tool' as const,
556
+ toolCallId: m.toolCallId!,
557
+ content: truncate(full),
558
+ fullContent: full,
559
+ isError: m.isError ?? false,
560
+ timestamp: m.timestamp,
561
+ }
562
+ }
563
+ case 'system': {
564
+ const full = contentToString(m.content)
565
+ return { role: 'system' as const, content: truncate(full), fullContent: full, timestamp: m.timestamp }
566
+ }
567
+ }
568
+ })
569
+
570
+ // Pending tool calls
571
+ const pendingToolCalls: ToolCallView[] = [
572
+ ...agent.pendingToolCalls.map(
573
+ (tc): ToolCallView => ({
574
+ id: tc.id,
575
+ name: tc.name,
576
+ input: tc.input,
577
+ status: 'pending' as const,
578
+ }),
579
+ ),
580
+ ...(agent.executingToolCall
581
+ ? [
582
+ {
583
+ id: agent.executingToolCall.toolCallId,
584
+ name: agent.executingToolCall.toolName,
585
+ input: undefined,
586
+ status: 'executing' as const,
587
+ } satisfies ToolCallView,
588
+ ]
589
+ : []),
590
+ ...agent.pendingToolResults.map(
591
+ (pr): ToolCallView => ({
592
+ id: pr.toolCallId,
593
+ name: pr.toolName,
594
+ input: undefined,
595
+ status: pr.isError ? 'failed' as const : 'completed' as const,
596
+ result: pr.isError ? undefined : contentToString(pr.content),
597
+ error: pr.isError ? contentToString(pr.content) : undefined,
598
+ }),
599
+ ),
600
+ ]
601
+
602
+ // Counters
603
+ const counters = state.agentCounters.get(agentId) ?? createDefaultCounters()
604
+
605
+ // Skills
606
+ const loadedSkills = state.agentSkills.get(agentId) ?? []
607
+
608
+ return {
609
+ id: agent.id,
610
+ definitionName: agent.definitionName,
611
+ status: toProtocolStatus(agent.status),
612
+ parentId: agent.parentId,
613
+ mailbox,
614
+ conversationHistory,
615
+ pendingToolCalls,
616
+ counters,
617
+ loadedSkills,
618
+ cost: agent.cost,
619
+ typedInput: agent.typedInput,
620
+ pauseReason: agent.pauseReason,
621
+ pauseMessage: agent.pauseMessage,
622
+ }
623
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Agent registry projection - tracks agent names and count.
3
+ *
4
+ * Minimal projection that replaces SessionState for name lookups.
5
+ * Handles only agent_spawned events.
6
+ */
7
+
8
+ import type { AgentId } from '@roj-ai/sdk'
9
+ import type { ProjectionEvent } from './events.js'
10
+
11
+ export interface AgentRegistryState {
12
+ names: Map<AgentId, string>
13
+ count: number
14
+ }
15
+
16
+ export function createAgentRegistryState(): AgentRegistryState {
17
+ return {
18
+ names: new Map(),
19
+ count: 0,
20
+ }
21
+ }
22
+
23
+ export function applyEventToAgentRegistry(state: AgentRegistryState, event: ProjectionEvent): AgentRegistryState {
24
+ if (event.type !== 'agent_spawned') return state
25
+
26
+ const newNames = new Map(state.names)
27
+ newNames.set(event.agentId, event.definitionName)
28
+
29
+ return {
30
+ names: newNames,
31
+ count: state.count + 1,
32
+ }
33
+ }
34
+
35
+ export function getAgentName(state: AgentRegistryState, agentId: AgentId | string): string {
36
+ return state.names.get(agentId as AgentId) ?? 'unknown'
37
+ }