@perstack/react 0.0.58 → 0.0.59

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 (45) hide show
  1. package/dist/src/index.d.ts +156 -3
  2. package/dist/src/index.js +660 -4
  3. package/dist/src/index.js.map +1 -1
  4. package/package.json +3 -3
  5. package/dist/src/hooks/index.d.ts +0 -4
  6. package/dist/src/hooks/index.d.ts.map +0 -1
  7. package/dist/src/hooks/index.js +0 -4
  8. package/dist/src/hooks/index.js.map +0 -1
  9. package/dist/src/hooks/use-job-stream.d.ts +0 -17
  10. package/dist/src/hooks/use-job-stream.d.ts.map +0 -1
  11. package/dist/src/hooks/use-job-stream.js +0 -45
  12. package/dist/src/hooks/use-job-stream.js.map +0 -1
  13. package/dist/src/hooks/use-job-streams.d.ts +0 -14
  14. package/dist/src/hooks/use-job-streams.d.ts.map +0 -1
  15. package/dist/src/hooks/use-job-streams.js +0 -89
  16. package/dist/src/hooks/use-job-streams.js.map +0 -1
  17. package/dist/src/hooks/use-run.d.ts +0 -38
  18. package/dist/src/hooks/use-run.d.ts.map +0 -1
  19. package/dist/src/hooks/use-run.js +0 -200
  20. package/dist/src/hooks/use-run.js.map +0 -1
  21. package/dist/src/index.d.ts.map +0 -1
  22. package/dist/src/types/index.d.ts +0 -2
  23. package/dist/src/types/index.d.ts.map +0 -1
  24. package/dist/src/types/index.js +0 -2
  25. package/dist/src/types/index.js.map +0 -1
  26. package/dist/src/types/runtime-state.d.ts +0 -19
  27. package/dist/src/types/runtime-state.d.ts.map +0 -1
  28. package/dist/src/types/runtime-state.js +0 -2
  29. package/dist/src/types/runtime-state.js.map +0 -1
  30. package/dist/src/utils/event-to-activity.d.ts +0 -67
  31. package/dist/src/utils/event-to-activity.d.ts.map +0 -1
  32. package/dist/src/utils/event-to-activity.js +0 -337
  33. package/dist/src/utils/event-to-activity.js.map +0 -1
  34. package/dist/src/utils/group-by-run.d.ts +0 -24
  35. package/dist/src/utils/group-by-run.d.ts.map +0 -1
  36. package/dist/src/utils/group-by-run.js +0 -43
  37. package/dist/src/utils/group-by-run.js.map +0 -1
  38. package/dist/src/utils/index.d.ts +0 -4
  39. package/dist/src/utils/index.d.ts.map +0 -1
  40. package/dist/src/utils/index.js +0 -4
  41. package/dist/src/utils/index.js.map +0 -1
  42. package/dist/src/utils/stream.d.ts +0 -13
  43. package/dist/src/utils/stream.d.ts.map +0 -1
  44. package/dist/src/utils/stream.js +0 -18
  45. package/dist/src/utils/stream.js.map +0 -1
package/dist/src/index.js CHANGED
@@ -1,5 +1,661 @@
1
- // Hooks
2
- export { useJobStream, useJobStreams, useRun, } from "./hooks/index.js";
3
- // Utils
4
- export { createInitialActivityProcessState, groupActivitiesByRun, processRunEventToActivity, toolToActivity, } from "./utils/index.js";
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { BASE_SKILL_PREFIX, createBaseToolActivity, createGeneralToolActivity } from "@perstack/core";
3
+
4
+ //#region src/utils/stream.ts
5
+ const isAbortError = (err) => err instanceof DOMException && err.name === "AbortError";
6
+ /**
7
+ * Consumes an SSE stream with abort-safe iteration and error normalization.
8
+ *
9
+ * Handles the connection lifecycle shared by all streaming hooks:
10
+ * 1. Calls `connect` to obtain an async iterable
11
+ * 2. Iterates events, stopping on signal abort
12
+ * 3. Filters out AbortError (normal cleanup), surfaces all others
13
+ */
14
+ async function consumeStream(connect, jobId, signal, onEvent) {
15
+ const events = await connect(jobId, signal);
16
+ for await (const event of events) {
17
+ if (signal.aborted) break;
18
+ onEvent(event);
19
+ }
20
+ }
21
+
22
+ //#endregion
23
+ //#region src/utils/event-to-activity.ts
24
+ /**
25
+ * Converts a tool call and result to an Activity.
26
+ * Delegates to core's createBaseToolActivity/createGeneralToolActivity to avoid duplication.
27
+ */
28
+ function toolToActivity(toolCall, toolResult, reasoning, meta) {
29
+ const { skillName, toolName } = toolCall;
30
+ return {
31
+ ...skillName.startsWith(BASE_SKILL_PREFIX) ? createBaseToolActivity(toolName, toolCall, toolResult, reasoning) : createGeneralToolActivity(skillName, toolName, toolCall, toolResult, reasoning),
32
+ id: meta.id,
33
+ expertKey: meta.expertKey,
34
+ runId: meta.runId,
35
+ previousActivityId: meta.previousActivityId,
36
+ delegatedBy: meta.delegatedBy
37
+ };
38
+ }
39
+ function createInitialActivityProcessState() {
40
+ return {
41
+ tools: /* @__PURE__ */ new Map(),
42
+ runStates: /* @__PURE__ */ new Map()
43
+ };
44
+ }
45
+ function getOrCreateRunState(state, runId, expertKey) {
46
+ let runState = state.runStates.get(runId);
47
+ if (!runState) {
48
+ runState = {
49
+ expertKey,
50
+ queryLogged: false,
51
+ completionLogged: false,
52
+ isComplete: false,
53
+ pendingDelegateCount: 0
54
+ };
55
+ state.runStates.set(runId, runState);
56
+ }
57
+ return runState;
58
+ }
59
+ const isRunEvent = (event) => "type" in event && "expertKey" in event;
60
+ const isCompleteStreamingReasoningEvent = (event) => "type" in event && event.type === "completeStreamingReasoning";
61
+ const isToolCallsEvent = (event) => event.type === "callTools" && "toolCalls" in event;
62
+ const isStopRunByDelegateEvent = (event) => event.type === "stopRunByDelegate" && "checkpoint" in event;
63
+ const isStopRunByInteractiveToolEvent = (event) => event.type === "stopRunByInteractiveTool" && "checkpoint" in event;
64
+ const isToolResultsEvent = (event) => event.type === "resolveToolResults" && "toolResults" in event;
65
+ /**
66
+ * Wraps multiple activities in a ParallelActivitiesGroup with shared reasoning.
67
+ * If only one activity, returns it directly.
68
+ */
69
+ function wrapInGroupIfParallel(activities, reasoning, meta) {
70
+ if (activities.length <= 1) return activities;
71
+ const activitiesWithoutReasoning = activities.map((a) => {
72
+ const { reasoning: _, ...rest } = a;
73
+ return rest;
74
+ });
75
+ return [{
76
+ type: "parallelGroup",
77
+ id: `parallel-${activities[0].id}`,
78
+ expertKey: meta.expertKey,
79
+ runId: meta.runId,
80
+ reasoning,
81
+ activities: activitiesWithoutReasoning
82
+ }];
83
+ }
84
+ /**
85
+ * Processes a RunEvent and produces Activity or ParallelActivitiesGroup items.
86
+ * Only processes RunEvent (state machine transitions), not RuntimeEvent.
87
+ *
88
+ * @param state - Mutable processing state
89
+ * @param event - The event to process
90
+ * @param addActivity - Callback to add a new Activity or ParallelActivitiesGroup
91
+ */
92
+ function processRunEventToActivity(state, event, addActivity) {
93
+ if (isCompleteStreamingReasoningEvent(event)) {
94
+ const { runId, text, expertKey } = event;
95
+ const runState = state.runStates.get(runId);
96
+ if (runState) runState.completedReasoning = text;
97
+ else state.runStates.set(runId, {
98
+ expertKey,
99
+ queryLogged: false,
100
+ completionLogged: false,
101
+ isComplete: false,
102
+ pendingDelegateCount: 0,
103
+ completedReasoning: text
104
+ });
105
+ return;
106
+ }
107
+ if (!isRunEvent(event)) return;
108
+ const runState = getOrCreateRunState(state, event.runId, event.expertKey);
109
+ if (event.type === "startRun") {
110
+ const startRunEvent = event;
111
+ const queryText = startRunEvent.inputMessages.find((m) => m.type === "userMessage")?.contents?.find((c) => c.type === "textPart")?.text;
112
+ if (!runState.delegatedBy) {
113
+ const delegatedByInfo = startRunEvent.initialCheckpoint?.delegatedBy;
114
+ if (delegatedByInfo) runState.delegatedBy = {
115
+ expertKey: delegatedByInfo.expert.key,
116
+ runId: delegatedByInfo.runId
117
+ };
118
+ }
119
+ const isDelegationReturn = startRunEvent.initialCheckpoint?.status === "stoppedByDelegate" || startRunEvent.initialCheckpoint?.status === "stoppedByInteractiveTool";
120
+ if (queryText && !runState.queryLogged && !isDelegationReturn) {
121
+ const activityId = `query-${event.runId}`;
122
+ addActivity({
123
+ type: "query",
124
+ id: activityId,
125
+ expertKey: event.expertKey,
126
+ runId: event.runId,
127
+ previousActivityId: runState.lastActivityId,
128
+ delegatedBy: runState.delegatedBy,
129
+ text: queryText
130
+ });
131
+ runState.lastActivityId = activityId;
132
+ runState.queryLogged = true;
133
+ }
134
+ return;
135
+ }
136
+ if (event.type === "resumeFromStop") {
137
+ if (!runState.delegatedBy) {
138
+ const delegatedByInfo = event.checkpoint?.delegatedBy;
139
+ if (delegatedByInfo) runState.delegatedBy = {
140
+ expertKey: delegatedByInfo.expert.key,
141
+ runId: delegatedByInfo.runId
142
+ };
143
+ }
144
+ return;
145
+ }
146
+ if (event.type === "retry") {
147
+ const retryEvent = event;
148
+ const activityId = `retry-${event.id}`;
149
+ addActivity({
150
+ type: "retry",
151
+ id: activityId,
152
+ expertKey: event.expertKey,
153
+ runId: event.runId,
154
+ previousActivityId: runState.lastActivityId,
155
+ delegatedBy: runState.delegatedBy,
156
+ reasoning: runState.completedReasoning,
157
+ error: retryEvent.reason,
158
+ message: ""
159
+ });
160
+ runState.lastActivityId = activityId;
161
+ runState.completedReasoning = void 0;
162
+ return;
163
+ }
164
+ if (event.type === "completeRun") {
165
+ if (!runState.completionLogged) {
166
+ const text = event.text ?? "";
167
+ const activityId = `completion-${event.runId}`;
168
+ addActivity({
169
+ type: "complete",
170
+ id: activityId,
171
+ expertKey: event.expertKey,
172
+ runId: event.runId,
173
+ previousActivityId: runState.lastActivityId,
174
+ delegatedBy: runState.delegatedBy,
175
+ reasoning: runState.completedReasoning,
176
+ text
177
+ });
178
+ runState.lastActivityId = activityId;
179
+ runState.completionLogged = true;
180
+ runState.isComplete = true;
181
+ runState.completedReasoning = void 0;
182
+ }
183
+ return;
184
+ }
185
+ if (event.type === "stopRunByError") {
186
+ const errorEvent = event;
187
+ const activityId = `error-${event.id}`;
188
+ addActivity({
189
+ type: "error",
190
+ id: activityId,
191
+ expertKey: event.expertKey,
192
+ runId: event.runId,
193
+ previousActivityId: runState.lastActivityId,
194
+ delegatedBy: runState.delegatedBy,
195
+ errorName: errorEvent.error.name,
196
+ error: errorEvent.error.message,
197
+ isRetryable: errorEvent.error.isRetryable
198
+ });
199
+ runState.lastActivityId = activityId;
200
+ runState.isComplete = true;
201
+ runState.completedReasoning = void 0;
202
+ return;
203
+ }
204
+ if (isToolCallsEvent(event)) {
205
+ for (const toolCall of event.toolCalls) if (!state.tools.has(toolCall.id)) state.tools.set(toolCall.id, {
206
+ id: toolCall.id,
207
+ toolCall,
208
+ logged: false
209
+ });
210
+ }
211
+ if (isStopRunByDelegateEvent(event)) {
212
+ const reasoning = runState.completedReasoning;
213
+ const delegations = event.checkpoint.delegateTo ?? [];
214
+ runState.pendingDelegateCount += delegations.length;
215
+ const delegateActivities = [];
216
+ for (const delegation of delegations) {
217
+ const existingTool = state.tools.get(delegation.toolCallId);
218
+ if (!existingTool || !existingTool.logged) {
219
+ if (existingTool) existingTool.logged = true;
220
+ const activityId = `delegate-${delegation.toolCallId}`;
221
+ delegateActivities.push({
222
+ type: "delegate",
223
+ id: activityId,
224
+ expertKey: event.expertKey,
225
+ runId: event.runId,
226
+ previousActivityId: runState.lastActivityId,
227
+ delegatedBy: runState.delegatedBy,
228
+ delegateExpertKey: delegation.expert.key,
229
+ query: delegation.query,
230
+ reasoning
231
+ });
232
+ runState.lastActivityId = activityId;
233
+ }
234
+ }
235
+ const wrapped = wrapInGroupIfParallel(delegateActivities, reasoning, {
236
+ expertKey: event.expertKey,
237
+ runId: event.runId,
238
+ delegatedBy: runState.delegatedBy
239
+ });
240
+ for (const item of wrapped) addActivity(item);
241
+ runState.completedReasoning = void 0;
242
+ return;
243
+ }
244
+ if (isStopRunByInteractiveToolEvent(event)) {
245
+ const reasoning = runState.completedReasoning;
246
+ const pendingToolCalls = event.checkpoint.pendingToolCalls ?? [];
247
+ const interactiveActivities = [];
248
+ for (const toolCall of pendingToolCalls) {
249
+ const existingTool = state.tools.get(toolCall.id);
250
+ if (!existingTool || !existingTool.logged) {
251
+ if (existingTool) existingTool.logged = true;
252
+ const activityId = `interactive-${toolCall.id}`;
253
+ interactiveActivities.push({
254
+ type: "interactiveTool",
255
+ id: activityId,
256
+ expertKey: event.expertKey,
257
+ runId: event.runId,
258
+ previousActivityId: runState.lastActivityId,
259
+ delegatedBy: runState.delegatedBy,
260
+ skillName: toolCall.skillName,
261
+ toolName: toolCall.toolName,
262
+ args: toolCall.args,
263
+ reasoning
264
+ });
265
+ runState.lastActivityId = activityId;
266
+ }
267
+ }
268
+ const wrapped = wrapInGroupIfParallel(interactiveActivities, reasoning, {
269
+ expertKey: event.expertKey,
270
+ runId: event.runId,
271
+ delegatedBy: runState.delegatedBy
272
+ });
273
+ for (const item of wrapped) addActivity(item);
274
+ runState.completedReasoning = void 0;
275
+ return;
276
+ }
277
+ if (isToolResultsEvent(event)) {
278
+ const reasoning = runState.completedReasoning;
279
+ const toolActivities = [];
280
+ for (const toolResult of event.toolResults) {
281
+ const tool = state.tools.get(toolResult.id);
282
+ if (tool && !tool.logged) {
283
+ const activityId = `action-${tool.id}`;
284
+ const activity = toolToActivity(tool.toolCall, toolResult, reasoning, {
285
+ id: activityId,
286
+ expertKey: event.expertKey,
287
+ runId: event.runId,
288
+ previousActivityId: runState.lastActivityId,
289
+ delegatedBy: runState.delegatedBy
290
+ });
291
+ toolActivities.push(activity);
292
+ runState.lastActivityId = activityId;
293
+ tool.logged = true;
294
+ }
295
+ }
296
+ if (toolActivities.length > 0) {
297
+ const wrapped = wrapInGroupIfParallel(toolActivities, reasoning, {
298
+ expertKey: event.expertKey,
299
+ runId: event.runId,
300
+ delegatedBy: runState.delegatedBy
301
+ });
302
+ for (const item of wrapped) addActivity(item);
303
+ runState.completedReasoning = void 0;
304
+ }
305
+ }
306
+ }
307
+
308
+ //#endregion
309
+ //#region src/hooks/use-run.ts
310
+ const STREAMING_EVENT_TYPES = new Set([
311
+ "startStreamingReasoning",
312
+ "streamReasoning",
313
+ "completeStreamingReasoning",
314
+ "startStreamingRunResult",
315
+ "streamRunResult",
316
+ "completeStreamingRunResult"
317
+ ]);
318
+ const isStreamingEvent = (event) => "type" in event && "expertKey" in event && STREAMING_EVENT_TYPES.has(event.type);
319
+ function processStreamingEvent(event, prevState) {
320
+ const { runId, expertKey } = event;
321
+ switch (event.type) {
322
+ case "startStreamingReasoning": return {
323
+ newState: {
324
+ ...prevState,
325
+ runs: {
326
+ ...prevState.runs,
327
+ [runId]: {
328
+ expertKey,
329
+ reasoning: "",
330
+ isReasoningActive: true
331
+ }
332
+ }
333
+ },
334
+ handled: true
335
+ };
336
+ case "streamReasoning": return {
337
+ newState: {
338
+ ...prevState,
339
+ runs: {
340
+ ...prevState.runs,
341
+ [runId]: {
342
+ ...prevState.runs[runId],
343
+ expertKey,
344
+ reasoning: (prevState.runs[runId]?.reasoning ?? "") + event.delta
345
+ }
346
+ }
347
+ },
348
+ handled: true
349
+ };
350
+ case "completeStreamingReasoning": return {
351
+ newState: {
352
+ ...prevState,
353
+ runs: {
354
+ ...prevState.runs,
355
+ [runId]: {
356
+ ...prevState.runs[runId],
357
+ expertKey,
358
+ isReasoningActive: false
359
+ }
360
+ }
361
+ },
362
+ handled: false
363
+ };
364
+ case "startStreamingRunResult": return {
365
+ newState: {
366
+ ...prevState,
367
+ runs: {
368
+ ...prevState.runs,
369
+ [runId]: {
370
+ expertKey,
371
+ reasoning: void 0,
372
+ isReasoningActive: false,
373
+ runResult: "",
374
+ isRunResultActive: true
375
+ }
376
+ }
377
+ },
378
+ handled: true
379
+ };
380
+ case "streamRunResult": return {
381
+ newState: {
382
+ ...prevState,
383
+ runs: {
384
+ ...prevState.runs,
385
+ [runId]: {
386
+ ...prevState.runs[runId],
387
+ expertKey,
388
+ runResult: (prevState.runs[runId]?.runResult ?? "") + event.delta
389
+ }
390
+ }
391
+ },
392
+ handled: true
393
+ };
394
+ case "completeStreamingRunResult": return {
395
+ newState: {
396
+ ...prevState,
397
+ runs: {
398
+ ...prevState.runs,
399
+ [runId]: {
400
+ ...prevState.runs[runId],
401
+ expertKey,
402
+ isRunResultActive: false
403
+ }
404
+ }
405
+ },
406
+ handled: true
407
+ };
408
+ default: return {
409
+ newState: prevState,
410
+ handled: false
411
+ };
412
+ }
413
+ }
414
+ /**
415
+ * Hook for managing Run state from RunEvent stream.
416
+ *
417
+ * Architecture:
418
+ * - ExpertStateEvent → ActivityOrGroup[] (accumulated, append-only)
419
+ * - StreamingEvent → StreamingState (latest only, for display)
420
+ *
421
+ * IMPORTANT: activities are append-only and never cleared.
422
+ * This is required for compatibility with Ink's <Static> component.
423
+ */
424
+ function useRun() {
425
+ const [activities, setActivities] = useState([]);
426
+ const [streaming, setStreaming] = useState({ runs: {} });
427
+ const [eventCount, setEventCount] = useState(0);
428
+ const [isComplete, setIsComplete] = useState(false);
429
+ const stateRef = useRef(createInitialActivityProcessState());
430
+ const clearStreaming = useCallback(() => {
431
+ setStreaming({ runs: {} });
432
+ }, []);
433
+ const handleStreamingEvent = useCallback((event) => {
434
+ let handled = false;
435
+ setStreaming((prev) => {
436
+ const result = processStreamingEvent(event, prev);
437
+ handled = result.handled;
438
+ return result.newState;
439
+ });
440
+ return handled;
441
+ }, []);
442
+ const processEvent = useCallback((event) => {
443
+ const newActivities = [];
444
+ const addActivity = (activity) => newActivities.push(activity);
445
+ processRunEventToActivity(stateRef.current, event, addActivity);
446
+ if (newActivities.length > 0) setActivities((prev) => [...prev, ...newActivities]);
447
+ setIsComplete(Array.from(stateRef.current.runStates.values()).some((rs) => rs.isComplete && !rs.delegatedBy));
448
+ }, []);
449
+ const clearRunStreaming = useCallback((runId) => {
450
+ setStreaming((prev) => {
451
+ const { [runId]: _, ...rest } = prev.runs;
452
+ return { runs: rest };
453
+ });
454
+ }, []);
455
+ return {
456
+ activities,
457
+ streaming,
458
+ isComplete,
459
+ eventCount,
460
+ addEvent: useCallback((event) => {
461
+ if (isStreamingEvent(event)) {
462
+ if (handleStreamingEvent(event)) {
463
+ setEventCount((prev) => prev + 1);
464
+ return;
465
+ }
466
+ }
467
+ if ("type" in event && "runId" in event && (event.type === "completeRun" || event.type === "stopRunByError")) clearRunStreaming(event.runId);
468
+ processEvent(event);
469
+ setEventCount((prev) => prev + 1);
470
+ }, [
471
+ handleStreamingEvent,
472
+ clearRunStreaming,
473
+ processEvent
474
+ ]),
475
+ appendHistoricalEvents: useCallback((historicalEvents) => {
476
+ const newActivities = [];
477
+ const addActivity = (activity) => newActivities.push(activity);
478
+ for (const event of historicalEvents) processRunEventToActivity(stateRef.current, event, addActivity);
479
+ if (newActivities.length > 0) setActivities((prev) => [...prev, ...newActivities]);
480
+ setEventCount((prev) => prev + historicalEvents.length);
481
+ setIsComplete(Array.from(stateRef.current.runStates.values()).some((rs) => rs.isComplete && !rs.delegatedBy));
482
+ }, []),
483
+ clearStreaming
484
+ };
485
+ }
486
+
487
+ //#endregion
488
+ //#region src/hooks/use-job-stream.ts
489
+ function useJobStream(options) {
490
+ const { jobId, connect, enabled = true } = options;
491
+ const shouldConnect = Boolean(jobId && enabled);
492
+ const { activities, streaming, addEvent } = useRun();
493
+ const [isConnected, setIsConnected] = useState(false);
494
+ const [error, setError] = useState(null);
495
+ const addEventRef = useRef(addEvent);
496
+ addEventRef.current = addEvent;
497
+ const connectRef = useRef(connect);
498
+ connectRef.current = connect;
499
+ useEffect(() => {
500
+ if (!shouldConnect || !jobId) {
501
+ setIsConnected(false);
502
+ return;
503
+ }
504
+ const controller = new AbortController();
505
+ async function run() {
506
+ setError(null);
507
+ setIsConnected(true);
508
+ try {
509
+ await consumeStream(connectRef.current, jobId, controller.signal, (event) => {
510
+ addEventRef.current(event);
511
+ });
512
+ } catch (err) {
513
+ if (isAbortError(err)) return;
514
+ setError(err instanceof Error ? err : new Error(String(err)));
515
+ } finally {
516
+ setIsConnected(false);
517
+ }
518
+ }
519
+ run();
520
+ return () => {
521
+ controller.abort();
522
+ };
523
+ }, [shouldConnect, jobId]);
524
+ return {
525
+ activities,
526
+ streaming,
527
+ latestActivity: useMemo(() => activities.length > 0 ? activities[activities.length - 1] : null, [activities]),
528
+ isConnected,
529
+ error
530
+ };
531
+ }
532
+
533
+ //#endregion
534
+ //#region src/hooks/use-job-streams.ts
535
+ function useJobStreams(options) {
536
+ const { jobs, connect } = options;
537
+ const [states, setStates] = useState(/* @__PURE__ */ new Map());
538
+ const controllersRef = useRef(/* @__PURE__ */ new Map());
539
+ const activityStateRef = useRef(/* @__PURE__ */ new Map());
540
+ const connectRef = useRef(connect);
541
+ connectRef.current = connect;
542
+ const connectToJob = useCallback(async (jobId, signal) => {
543
+ setStates((prev) => {
544
+ const next = new Map(prev);
545
+ next.set(jobId, {
546
+ latestActivity: null,
547
+ isConnected: true
548
+ });
549
+ return next;
550
+ });
551
+ let state = activityStateRef.current.get(jobId);
552
+ if (!state) {
553
+ state = createInitialActivityProcessState();
554
+ activityStateRef.current.set(jobId, state);
555
+ }
556
+ try {
557
+ await consumeStream(connectRef.current, jobId, signal, (event) => {
558
+ let latestActivity = null;
559
+ processRunEventToActivity(state, event, (activity) => {
560
+ latestActivity = activity;
561
+ });
562
+ if (latestActivity) setStates((prev) => {
563
+ const next = new Map(prev);
564
+ next.set(jobId, {
565
+ latestActivity,
566
+ isConnected: true
567
+ });
568
+ return next;
569
+ });
570
+ });
571
+ } catch (err) {
572
+ if (isAbortError(err)) return;
573
+ console.error(`Stream connection failed for job ${jobId}:`, err);
574
+ } finally {
575
+ setStates((prev) => {
576
+ const current = prev.get(jobId);
577
+ if (!current) return prev;
578
+ const next = new Map(prev);
579
+ next.set(jobId, {
580
+ ...current,
581
+ isConnected: false
582
+ });
583
+ return next;
584
+ });
585
+ }
586
+ }, []);
587
+ useEffect(() => {
588
+ const enabledIds = new Set(jobs.filter((j) => j.enabled).map((j) => j.id));
589
+ for (const [jobId, controller] of controllersRef.current) if (!enabledIds.has(jobId)) {
590
+ controller.abort();
591
+ controllersRef.current.delete(jobId);
592
+ activityStateRef.current.delete(jobId);
593
+ setStates((prev) => {
594
+ const next = new Map(prev);
595
+ next.delete(jobId);
596
+ return next;
597
+ });
598
+ }
599
+ for (const jobId of enabledIds) if (!controllersRef.current.has(jobId)) {
600
+ const controller = new AbortController();
601
+ controllersRef.current.set(jobId, controller);
602
+ connectToJob(jobId, controller.signal);
603
+ }
604
+ }, [jobs, connectToJob]);
605
+ useEffect(() => {
606
+ return () => {
607
+ for (const controller of controllersRef.current.values()) controller.abort();
608
+ controllersRef.current.clear();
609
+ activityStateRef.current.clear();
610
+ };
611
+ }, []);
612
+ return states;
613
+ }
614
+
615
+ //#endregion
616
+ //#region src/utils/group-by-run.ts
617
+ /**
618
+ * Extract base properties from ActivityOrGroup for grouping.
619
+ */
620
+ function getActivityProps(activityOrGroup) {
621
+ if (activityOrGroup.type === "parallelGroup") {
622
+ const firstActivity = activityOrGroup.activities[0];
623
+ return {
624
+ runId: activityOrGroup.runId,
625
+ expertKey: activityOrGroup.expertKey,
626
+ delegatedBy: firstActivity?.delegatedBy
627
+ };
628
+ }
629
+ return activityOrGroup;
630
+ }
631
+ /**
632
+ * Groups activities by their runId while preserving order.
633
+ *
634
+ * This function groups activities so that each run can be displayed in its own
635
+ * visual section (e.g., separate <Static> components in Ink).
636
+ *
637
+ * @param activities - Array of activities or groups to group
638
+ * @returns Array of RunGroup objects, ordered by first appearance
639
+ */
640
+ function groupActivitiesByRun(activities) {
641
+ const groupMap = /* @__PURE__ */ new Map();
642
+ const order = [];
643
+ for (const activityOrGroup of activities) {
644
+ const { runId, expertKey, delegatedBy } = getActivityProps(activityOrGroup);
645
+ if (!groupMap.has(runId)) {
646
+ groupMap.set(runId, {
647
+ runId,
648
+ expertKey,
649
+ activities: [],
650
+ delegatedBy
651
+ });
652
+ order.push(runId);
653
+ }
654
+ groupMap.get(runId).activities.push(activityOrGroup);
655
+ }
656
+ return order.map((runId) => groupMap.get(runId));
657
+ }
658
+
659
+ //#endregion
660
+ export { createInitialActivityProcessState, groupActivitiesByRun, processRunEventToActivity, toolToActivity, useJobStream, useJobStreams, useRun };
5
661
  //# sourceMappingURL=index.js.map