@pikku/assistant-ui 0.12.4 → 0.12.5

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.
@@ -87,6 +87,17 @@ type ToolCall = {
87
87
  isError?: boolean
88
88
  }
89
89
 
90
+ type StructuredPart =
91
+ | {
92
+ type: 'generative-ui'
93
+ spec: unknown
94
+ }
95
+ | {
96
+ type: 'data'
97
+ name: string
98
+ data: unknown
99
+ }
100
+
90
101
  /**
91
102
  * Shared helper: consume an SSE stream and populate text/toolCalls.
92
103
  * Returns an array of PendingApprovals when the stream requests them, or empty when done.
@@ -95,6 +106,7 @@ async function processStream(
95
106
  reader: ReadableStreamDefaultReader<Uint8Array>,
96
107
  text: { value: string },
97
108
  toolCalls: ToolCall[],
109
+ structuredParts: StructuredPart[],
98
110
  yieldContent: () => void,
99
111
  onFinish?: () => void
100
112
  ): Promise<PendingApproval[]> {
@@ -140,6 +152,31 @@ async function processStream(
140
152
  }
141
153
  break
142
154
  }
155
+ case 'generative-ui': {
156
+ const nextPart = {
157
+ type: 'generative-ui' as const,
158
+ spec: event.spec,
159
+ }
160
+ const existingIndex = structuredParts.findIndex(
161
+ (part) => part.type === 'generative-ui'
162
+ )
163
+ if (existingIndex === -1) structuredParts.push(nextPart)
164
+ else structuredParts[existingIndex] = nextPart
165
+ break
166
+ }
167
+ case 'data': {
168
+ const nextPart = {
169
+ type: 'data' as const,
170
+ name: event.name,
171
+ data: event.data,
172
+ }
173
+ const existingIndex = structuredParts.findIndex(
174
+ (part) => part.type === 'data' && part.name === event.name
175
+ )
176
+ if (existingIndex === -1) structuredParts.push(nextPart)
177
+ else structuredParts[existingIndex] = nextPart
178
+ break
179
+ }
143
180
  case 'approval-request':
144
181
  pendingApprovals.push({
145
182
  toolCallId: event.toolCallId,
@@ -237,13 +274,39 @@ export function resolvePikkuToolStatus(
237
274
  return { type: 'completed' }
238
275
  }
239
276
 
240
- function buildContent(text: { value: string }, toolCalls: ToolCall[]): any[] {
277
+ function buildRichContent(
278
+ text: { value: string },
279
+ structuredParts: StructuredPart[],
280
+ toolCalls: ToolCall[]
281
+ ): any[] {
241
282
  const content: any[] = []
242
283
  if (text.value) content.push({ type: 'text' as const, text: text.value })
284
+ content.push(...structuredParts)
243
285
  content.push(...toolCalls)
244
286
  return content
245
287
  }
246
288
 
289
+ function buildContentFromAgentResult(result: unknown): any[] {
290
+ const content: any[] = []
291
+
292
+ if (typeof result === 'string') {
293
+ if (result) content.push({ type: 'text' as const, text: result })
294
+ return content
295
+ }
296
+
297
+ if (!result || typeof result !== 'object') return content
298
+
299
+ const record = result as Record<string, unknown>
300
+ if (typeof record.text === 'string' && record.text) {
301
+ content.push({ type: 'text' as const, text: record.text })
302
+ }
303
+ if (record.ui != null) {
304
+ content.push({ type: 'generative-ui' as const, spec: record.ui })
305
+ }
306
+
307
+ return content
308
+ }
309
+
247
310
  function createPikkuStreamingAdapter(
248
311
  optionsRef: React.RefObject<PikkuAgentRuntimeOptions>,
249
312
  pendingApprovalsRef: React.RefObject<PendingApproval[]>,
@@ -281,6 +344,7 @@ function createPikkuStreamingAdapter(
281
344
  // The last resume triggers continuation (next LLM step).
282
345
  let lastText = { value: '' }
283
346
  let lastToolCalls: ToolCall[] = []
347
+ let lastStructuredParts: StructuredPart[] = []
284
348
  let nextApprovals: PendingApproval[] = []
285
349
 
286
350
  for (let i = 0; i < decisions.length; i++) {
@@ -320,11 +384,13 @@ function createPikkuStreamingAdapter(
320
384
 
321
385
  const text = { value: '' }
322
386
  const toolCalls: ToolCall[] = []
387
+ const structuredParts: StructuredPart[] = []
323
388
  const reader = resumeResponse.body.getReader()
324
389
  const streamApprovals = await processStream(
325
390
  reader,
326
391
  text,
327
392
  toolCalls,
393
+ structuredParts,
328
394
  () => {},
329
395
  i === decisions.length - 1
330
396
  ? (onFinishRef.current ?? undefined)
@@ -334,13 +400,18 @@ function createPikkuStreamingAdapter(
334
400
  // Keep the last resume's output (it has continuation content)
335
401
  lastText = text
336
402
  lastToolCalls = toolCalls
403
+ lastStructuredParts = structuredParts
337
404
  if (streamApprovals.length > 0) {
338
405
  nextApprovals = streamApprovals
339
406
  }
340
407
  }
341
408
 
342
409
  // Build content from the last resume's output
343
- const content = buildContent(lastText, lastToolCalls)
410
+ const content = buildRichContent(
411
+ lastText,
412
+ lastStructuredParts,
413
+ lastToolCalls
414
+ )
344
415
 
345
416
  if (nextApprovals.length > 0) {
346
417
  // More approvals from continuation — show them
@@ -372,7 +443,11 @@ function createPikkuStreamingAdapter(
372
443
  }
373
444
  }
374
445
 
375
- const updatedContent = buildContent(lastText, lastToolCalls)
446
+ const updatedContent = buildRichContent(
447
+ lastText,
448
+ lastStructuredParts,
449
+ lastToolCalls
450
+ )
376
451
  yield {
377
452
  content: updatedContent,
378
453
  status: {
@@ -451,9 +526,10 @@ function createPikkuStreamingAdapter(
451
526
 
452
527
  const text = { value: '' }
453
528
  const toolCalls: ToolCall[] = []
529
+ const structuredParts: StructuredPart[] = []
454
530
  let pendingContent: any[] | null = null
455
531
  const yieldContent = () => {
456
- const content = buildContent(text, toolCalls)
532
+ const content = buildRichContent(text, structuredParts, toolCalls)
457
533
  if (content.length > 0) pendingContent = content
458
534
  }
459
535
 
@@ -462,6 +538,7 @@ function createPikkuStreamingAdapter(
462
538
  reader,
463
539
  text,
464
540
  toolCalls,
541
+ structuredParts,
465
542
  yieldContent,
466
543
  onFinishRef.current ?? undefined
467
544
  )
@@ -516,7 +593,7 @@ function createPikkuStreamingAdapter(
516
593
  }
517
594
  }
518
595
 
519
- const content = buildContent(text, toolCalls)
596
+ const content = buildRichContent(text, structuredParts, toolCalls)
520
597
  yield {
521
598
  content,
522
599
  status: {
@@ -596,7 +673,11 @@ export const convertDbMessages = (dbMessages: any[]): ThreadMessageLike[] => {
596
673
  const parts: any[] = []
597
674
 
598
675
  if (msg.content) {
599
- parts.push({ type: 'text' as const, text: msg.content })
676
+ if (Array.isArray(msg.content)) {
677
+ parts.push(...msg.content)
678
+ } else {
679
+ parts.push({ type: 'text' as const, text: msg.content })
680
+ }
600
681
  }
601
682
 
602
683
  if (Array.isArray(msg.toolCalls)) {
@@ -732,6 +813,7 @@ function createPikkuNonStreamingAdapter(
732
813
  // Resume uses SSE (same as streaming mode)
733
814
  let lastText = { value: '' }
734
815
  let lastToolCalls: ToolCall[] = []
816
+ let lastStructuredParts: StructuredPart[] = []
735
817
  let nextApprovals: PendingApproval[] = []
736
818
 
737
819
  for (let i = 0; i < decisions.length; i++) {
@@ -770,11 +852,13 @@ function createPikkuNonStreamingAdapter(
770
852
 
771
853
  const text = { value: '' }
772
854
  const toolCalls: ToolCall[] = []
855
+ const structuredParts: StructuredPart[] = []
773
856
  const reader = resumeResponse.body.getReader()
774
857
  const streamApprovals = await processStream(
775
858
  reader,
776
859
  text,
777
860
  toolCalls,
861
+ structuredParts,
778
862
  () => {},
779
863
  i === decisions.length - 1
780
864
  ? (onFinishRef.current ?? undefined)
@@ -783,12 +867,17 @@ function createPikkuNonStreamingAdapter(
783
867
 
784
868
  lastText = text
785
869
  lastToolCalls = toolCalls
870
+ lastStructuredParts = structuredParts
786
871
  if (streamApprovals.length > 0) {
787
872
  nextApprovals = streamApprovals
788
873
  }
789
874
  }
790
875
 
791
- const content = buildContent(lastText, lastToolCalls)
876
+ const content = buildRichContent(
877
+ lastText,
878
+ lastStructuredParts,
879
+ lastToolCalls
880
+ )
792
881
 
793
882
  if (nextApprovals.length > 0) {
794
883
  pendingApprovalsRef.current = nextApprovals
@@ -818,7 +907,11 @@ function createPikkuNonStreamingAdapter(
818
907
  }
819
908
  }
820
909
 
821
- const updatedContent = buildContent(lastText, lastToolCalls)
910
+ const updatedContent = buildRichContent(
911
+ lastText,
912
+ lastStructuredParts,
913
+ lastToolCalls
914
+ )
822
915
  yield {
823
916
  content: updatedContent,
824
917
  status: {
@@ -884,9 +977,7 @@ function createPikkuNonStreamingAdapter(
884
977
  }))
885
978
 
886
979
  const content: any[] = []
887
- if (json.result) {
888
- content.push({ type: 'text' as const, text: json.result })
889
- }
980
+ content.push(...buildContentFromAgentResult(json.result))
890
981
  content.push(...toolCalls)
891
982
 
892
983
  yield {
@@ -901,10 +992,7 @@ function createPikkuNonStreamingAdapter(
901
992
 
902
993
  // No approvals — yield complete content
903
994
  onFinishRef.current?.()
904
- const content: any[] = []
905
- if (json.result) {
906
- content.push({ type: 'text' as const, text: String(json.result) })
907
- }
995
+ const content = buildContentFromAgentResult(json.result)
908
996
  if (content.length > 0) {
909
997
  yield { content }
910
998
  }