experimental-a2 0.18.0 → 0.19.0

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 (152) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/{actor-Bjce9_5U.d.ts → actor-C7b-1arn.d.ts} +3 -3
  3. package/dist/{actor-Bjce9_5U.d.ts.map → actor-C7b-1arn.d.ts.map} +1 -1
  4. package/dist/actor-client.d.ts +1 -1
  5. package/dist/actor-react.d.ts +1 -1
  6. package/dist/actor-react.js +1 -1
  7. package/dist/actor.d.ts +1 -1
  8. package/dist/actor.js +2 -2
  9. package/dist/{ai-DrUaMyRt.d.ts → ai-Cz3hqVhk.d.ts} +34 -17
  10. package/dist/ai-Cz3hqVhk.d.ts.map +1 -0
  11. package/dist/ai-client.d.ts +1 -1
  12. package/dist/ai-client.js +2 -2
  13. package/dist/ai-control-CvLbW10B.js +46 -0
  14. package/dist/ai-control-CvLbW10B.js.map +1 -0
  15. package/dist/ai-fx.d.ts +1 -1
  16. package/dist/ai-fx.d.ts.map +1 -1
  17. package/dist/ai-fx.js +122 -44
  18. package/dist/ai-fx.js.map +1 -1
  19. package/dist/{ai-harness-BuLh_EYi.js → ai-harness-BuuC-gJ3.js} +79 -15
  20. package/dist/ai-harness-BuuC-gJ3.js.map +1 -0
  21. package/dist/{ai-harness-DvsPeJeB.d.ts → ai-harness-CV6rXbB3.d.ts} +2 -2
  22. package/dist/ai-harness-CV6rXbB3.d.ts.map +1 -0
  23. package/dist/{ai-BpOGm-zB.js → ai-mMiWul1T.js} +117 -78
  24. package/dist/ai-mMiWul1T.js.map +1 -0
  25. package/dist/ai-pi.d.ts +1 -1
  26. package/dist/ai-pi.d.ts.map +1 -1
  27. package/dist/ai-pi.js +114 -23
  28. package/dist/ai-pi.js.map +1 -1
  29. package/dist/ai-react.d.ts +1 -1
  30. package/dist/ai-react.js +2 -2
  31. package/dist/ai-server.d.ts +3 -3
  32. package/dist/ai-server.d.ts.map +1 -1
  33. package/dist/ai-server.js +127 -55
  34. package/dist/ai-server.js.map +1 -1
  35. package/dist/ai-tool-input-DmSqMUQi.js +40 -0
  36. package/dist/ai-tool-input-DmSqMUQi.js.map +1 -0
  37. package/dist/ai.d.ts +2 -2
  38. package/dist/ai.js +1 -1
  39. package/dist/{client-BReKqwZC.js → client-BhHLoNKF.js} +3 -3
  40. package/dist/{client-BReKqwZC.js.map → client-BhHLoNKF.js.map} +1 -1
  41. package/dist/client.js +1 -1
  42. package/dist/index.d.ts +2 -2
  43. package/dist/{internal-B2T3U4Rq.js → internal-CscSY7K_.js} +4 -2
  44. package/dist/{internal-B2T3U4Rq.js.map → internal-CscSY7K_.js.map} +1 -1
  45. package/dist/react.js +1 -1
  46. package/dist/scheduler-qstash.d.ts +2 -2
  47. package/dist/scheduler-qstash.js +3 -3
  48. package/dist/scheduler-qstash.js.map +1 -1
  49. package/dist/scheduler-vercel.d.ts +7 -7
  50. package/dist/scheduler-vercel.d.ts.map +1 -1
  51. package/dist/scheduler-vercel.js +10 -14
  52. package/dist/scheduler-vercel.js.map +1 -1
  53. package/dist/{server-Dw_k_7kO.js → server-D-Y4pqwu.js} +37 -11
  54. package/dist/server-D-Y4pqwu.js.map +1 -0
  55. package/dist/{server-En7cbi3v.d.ts → server-DYAATFuT.d.ts} +11 -5
  56. package/dist/server-DYAATFuT.d.ts.map +1 -0
  57. package/dist/server.d.ts +2 -2
  58. package/dist/server.js +1 -1
  59. package/dist/{session-clear-Bo-XTFEy.d.ts → session-clear-C-QwTXV1.d.ts} +2 -2
  60. package/dist/session-clear-C-QwTXV1.d.ts.map +1 -0
  61. package/dist/{store-Djdv0bnZ.d.ts → store-CyLUy86v.d.ts} +23 -4
  62. package/dist/store-CyLUy86v.d.ts.map +1 -0
  63. package/dist/store-N8PXxDAS.js.map +1 -1
  64. package/dist/store-memory.d.ts +1 -1
  65. package/dist/store-memory.js +25 -6
  66. package/dist/store-memory.js.map +1 -1
  67. package/dist/{store-polling-B5FcwJfo.js → store-polling-7WJV0YWP.js} +2 -2
  68. package/dist/{store-polling-B5FcwJfo.js.map → store-polling-7WJV0YWP.js.map} +1 -1
  69. package/dist/store-postgres.d.ts +1 -1
  70. package/dist/store-postgres.d.ts.map +1 -1
  71. package/dist/store-postgres.js +55 -25
  72. package/dist/store-postgres.js.map +1 -1
  73. package/dist/{store-presence-polling-BAwS8RWn.js → store-presence-polling-B1ixeqIS.js} +3 -3
  74. package/dist/{store-presence-polling-BAwS8RWn.js.map → store-presence-polling-B1ixeqIS.js.map} +1 -1
  75. package/dist/store-redis-http.d.ts +1 -1
  76. package/dist/store-redis-http.js +3 -3
  77. package/dist/{store-redis-notify-BRITti_o.js → store-redis-notify-CM0AfLY0.js} +88 -37
  78. package/dist/store-redis-notify-CM0AfLY0.js.map +1 -0
  79. package/dist/store-redis.d.ts +1 -1
  80. package/dist/store-redis.js +1 -1
  81. package/dist/store-sqlite.d.ts +1 -1
  82. package/dist/store-sqlite.js +35 -20
  83. package/dist/store-sqlite.js.map +1 -1
  84. package/dist/{wire-rSUglYo8.js → wire-BKxH1vbP.js} +2 -2
  85. package/dist/{wire-rSUglYo8.js.map → wire-BKxH1vbP.js.map} +1 -1
  86. package/docs/agents/03-conversations.mdx +27 -15
  87. package/docs/agents/04-harnesses.mdx +51 -5
  88. package/docs/contract/03-sessions.mdx +1 -1
  89. package/docs/index.mdx +3 -3
  90. package/docs/reference/02-server.mdx +12 -7
  91. package/docs/reference/04-ai.mdx +41 -23
  92. package/docs/reference/05-adapters.mdx +6 -6
  93. package/docs/server/01-handlers.mdx +1 -1
  94. package/docs/server/02-timers.mdx +1 -1
  95. package/docs/server/03-cancellation.mdx +3 -2
  96. package/docs/server/05-production.mdx +4 -3
  97. package/docs/server/06-guarantees.mdx +20 -11
  98. package/examples/playground/DEPLOY.md +11 -4
  99. package/examples/playground/app/a0/[appId]/page.tsx +2 -8
  100. package/examples/playground/app/a0/a0.css +244 -5
  101. package/examples/playground/app/a0/app-view.tsx +2 -1
  102. package/examples/playground/app/a0/attachments.ts +133 -0
  103. package/examples/playground/app/a0/chat.test.tsx +504 -0
  104. package/examples/playground/app/a0/chat.tsx +623 -63
  105. package/examples/playground/app/a0/context-menu.tsx +38 -11
  106. package/examples/playground/app/a0/devtools.tsx +2 -8
  107. package/examples/playground/app/a0/model.test.ts +28 -0
  108. package/examples/playground/app/a0/model.ts +30 -0
  109. package/examples/playground/app/a0/server.ts +13 -19
  110. package/examples/playground/app/a0/uploads/route.ts +29 -0
  111. package/examples/playground/app/agent/[agentId]/agent-client.tsx +2 -1
  112. package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +1 -0
  113. package/examples/playground/app/agent/model.test.ts +12 -3
  114. package/examples/playground/app/chat/[chatId]/agent-state.test.ts +1 -5
  115. package/examples/playground/next.config.ts +7 -4
  116. package/examples/playground/package.json +3 -1
  117. package/package.json +1 -1
  118. package/src/ai-agent.ts +14 -3
  119. package/src/ai-client-state.ts +21 -9
  120. package/src/ai-context-schema.ts +28 -4
  121. package/src/ai-context.ts +83 -3
  122. package/src/ai-control-server.ts +70 -43
  123. package/src/ai-control-state.ts +3 -4
  124. package/src/ai-control.ts +59 -10
  125. package/src/ai-coordinator.ts +7 -6
  126. package/src/ai-fx.ts +184 -67
  127. package/src/ai-harness.ts +153 -29
  128. package/src/ai-message-shape.ts +41 -0
  129. package/src/ai-pi.ts +161 -46
  130. package/src/ai-stored-state.ts +1 -3
  131. package/src/ai-tool-input.ts +45 -0
  132. package/src/ai.ts +116 -77
  133. package/src/internal.ts +6 -0
  134. package/src/libfx.d.ts +8 -1
  135. package/src/scheduler-qstash.ts +5 -5
  136. package/src/scheduler-vercel.ts +15 -27
  137. package/src/server.ts +58 -12
  138. package/src/store-memory.ts +50 -9
  139. package/src/store-postgres.ts +79 -28
  140. package/src/store-redis-core.ts +112 -31
  141. package/src/store-sqlite.ts +61 -28
  142. package/src/store.ts +22 -3
  143. package/dist/ai-BpOGm-zB.js.map +0 -1
  144. package/dist/ai-DrUaMyRt.d.ts.map +0 -1
  145. package/dist/ai-harness-BuLh_EYi.js.map +0 -1
  146. package/dist/ai-harness-DvsPeJeB.d.ts.map +0 -1
  147. package/dist/server-Dw_k_7kO.js.map +0 -1
  148. package/dist/server-En7cbi3v.d.ts.map +0 -1
  149. package/dist/session-clear-Bo-XTFEy.d.ts.map +0 -1
  150. package/dist/store-Djdv0bnZ.d.ts.map +0 -1
  151. package/dist/store-redis-notify-BRITti_o.js.map +0 -1
  152. package/examples/playground/app/a0/no-photon.cjs +0 -4
@@ -345,10 +345,140 @@ body:has(.a0-splitter[data-dragging]) iframe {
345
345
  .a0-text[data-role='user'] {
346
346
  align-self: flex-end;
347
347
  padding: 0.5rem 0.75rem;
348
+ overflow-wrap: anywhere;
348
349
  background: var(--shade);
349
350
  color: var(--ink);
350
351
  }
351
352
 
353
+ /* ── attachments ─────────────────────────────────────────────────── */
354
+
355
+ .a0-chat-drop {
356
+ display: contents;
357
+ }
358
+
359
+ /* Over the whole chat column, which positions it. */
360
+ .a0-drop {
361
+ position: absolute;
362
+ z-index: 2;
363
+ inset: 0;
364
+ display: flex;
365
+ align-items: center;
366
+ justify-content: center;
367
+ gap: 0.5rem;
368
+ background: color-mix(in oklab, var(--bg) 85%, transparent);
369
+ outline: 2px dashed var(--muted);
370
+ outline-offset: -0.75rem;
371
+ color: var(--bright);
372
+ pointer-events: none;
373
+ }
374
+
375
+ .a0-files {
376
+ display: flex;
377
+ flex-wrap: wrap;
378
+ gap: 0.375rem;
379
+ max-width: 85%;
380
+ }
381
+
382
+ .a0-files[data-role='user'] {
383
+ align-self: flex-end;
384
+ justify-content: flex-end;
385
+ }
386
+
387
+ .a0-files[data-role='assistant'] {
388
+ align-self: flex-start;
389
+ }
390
+
391
+ .a0-files img {
392
+ display: block;
393
+ max-width: 12rem;
394
+ max-height: 9rem;
395
+ border: 1px solid var(--line);
396
+ object-fit: cover;
397
+ }
398
+
399
+ .a0-file {
400
+ display: inline-flex;
401
+ align-items: center;
402
+ gap: 0.25rem;
403
+ color: inherit;
404
+ text-decoration: underline dotted var(--faint);
405
+ text-underline-offset: 3px;
406
+ vertical-align: bottom;
407
+ }
408
+
409
+ .a0-files .a0-file {
410
+ padding: 0.25rem 0.5rem;
411
+ border: 1px dashed var(--line);
412
+ text-decoration: none;
413
+ }
414
+
415
+ .a0-attachments {
416
+ display: flex;
417
+ flex-wrap: wrap;
418
+ gap: 0.375rem;
419
+ margin: 0;
420
+ padding: 0.625rem 0.75rem 0;
421
+ list-style: none;
422
+ font-size: 0.75rem;
423
+ }
424
+
425
+ .a0-attachments li {
426
+ position: relative;
427
+ display: inline-flex;
428
+ align-items: center;
429
+ gap: 0.25rem;
430
+ min-width: 0;
431
+ max-width: 100%;
432
+ color: var(--muted);
433
+ }
434
+
435
+ .a0-attachments li[data-kind='link'] {
436
+ height: 1.75rem;
437
+ padding-left: 0.5rem;
438
+ border: 1px dashed var(--line);
439
+ }
440
+
441
+ .a0-attachments li[data-kind='link'] span {
442
+ overflow: hidden;
443
+ text-overflow: ellipsis;
444
+ white-space: nowrap;
445
+ }
446
+
447
+ .a0-attachments img {
448
+ display: block;
449
+ width: 3rem;
450
+ height: 3rem;
451
+ border: 1px solid var(--line);
452
+ object-fit: cover;
453
+ }
454
+
455
+ .a0-attachments li[data-kind='image'][data-uploading] img {
456
+ opacity: 0.5;
457
+ }
458
+
459
+ /* On the thumbnail's corners: the spinner, and the button that removes it. */
460
+ .a0-attachments li[data-kind='image'] .a0-spin {
461
+ position: absolute;
462
+ top: calc(50% - 6px);
463
+ left: calc(1.5rem - 6px);
464
+ }
465
+
466
+ .a0-attachments li[data-kind='image'] .a0-icon-button {
467
+ position: absolute;
468
+ top: -0.375rem;
469
+ right: -0.375rem;
470
+ width: 1.125rem;
471
+ height: 1.125rem;
472
+ border: 1px solid var(--line);
473
+ border-radius: 50%;
474
+ background: var(--bg);
475
+ }
476
+
477
+ .a0-attachments li[data-kind='link'] .a0-icon-button {
478
+ width: 1.5rem;
479
+ height: 1.5rem;
480
+ }
481
+
352
482
  .a0-text[data-role='assistant'] {
353
483
  align-self: flex-start;
354
484
  color: var(--muted);
@@ -428,6 +558,86 @@ body:has(.a0-splitter[data-dragging]) iframe {
428
558
  white-space: nowrap;
429
559
  }
430
560
 
561
+ /* A tool step that shows what the call writes, folded once it's done. */
562
+ .a0-tool {
563
+ align-self: stretch;
564
+ min-width: 0;
565
+ }
566
+
567
+ .a0-tool summary {
568
+ width: fit-content;
569
+ cursor: pointer;
570
+ list-style: none;
571
+ }
572
+
573
+ .a0-tool summary::-webkit-details-marker {
574
+ display: none;
575
+ }
576
+
577
+ .a0-tool summary:hover .a0-step-label {
578
+ color: var(--ink);
579
+ }
580
+
581
+ .a0-tool[data-failed] summary,
582
+ .a0-tool[data-failed] .a0-step-label {
583
+ color: var(--danger);
584
+ }
585
+
586
+ .a0-tool-text {
587
+ max-height: 14rem;
588
+ margin: 0.375rem 0 0 1.125rem;
589
+ padding: 0.5rem 0.625rem;
590
+ overflow: auto;
591
+ background: color-mix(in oklab, var(--shade) 60%, transparent);
592
+ color: var(--muted);
593
+ font-size: 0.6875rem;
594
+ line-height: 1.5;
595
+ overflow-wrap: anywhere;
596
+ scrollbar-color: var(--faint) transparent;
597
+ scrollbar-width: thin;
598
+ white-space: pre-wrap;
599
+ }
600
+
601
+ /* Messages waiting for the turn to end, just above the composer. */
602
+ .a0-queue {
603
+ display: flex;
604
+ flex-shrink: 0;
605
+ flex-direction: column;
606
+ gap: 0.25rem;
607
+ margin: 0 0.75rem;
608
+ padding: 0;
609
+ list-style: none;
610
+ font-size: 0.75rem;
611
+ }
612
+
613
+ .a0-queue li {
614
+ display: flex;
615
+ align-items: center;
616
+ gap: 0.375rem;
617
+ padding: 0.25rem 0.25rem 0.25rem 0.625rem;
618
+ border: 1px dashed var(--line);
619
+ color: var(--muted);
620
+ }
621
+
622
+ .a0-queue span {
623
+ flex: 1;
624
+ min-width: 0;
625
+ overflow: hidden;
626
+ text-overflow: ellipsis;
627
+ white-space: nowrap;
628
+ }
629
+
630
+ .a0-queue button {
631
+ height: 1.375rem;
632
+ padding: 0 0.375rem;
633
+ }
634
+
635
+ .a0-queue .a0-icon-button {
636
+ width: 1.375rem;
637
+ padding: 0;
638
+ color: var(--faint);
639
+ }
640
+
431
641
  .a0-send-error {
432
642
  margin: 0;
433
643
  padding: 0 1.25rem 0.5rem;
@@ -533,16 +743,45 @@ body:has(.a0-splitter[data-dragging]) iframe {
533
743
  gap: 0.5rem;
534
744
  }
535
745
 
536
- .a0-queued {
537
- color: var(--faint);
538
- font-size: 0.75rem;
539
- }
540
-
541
746
  .a0-composer-actions button {
542
747
  height: 1.75rem;
543
748
  padding: 0 0.625rem;
544
749
  }
545
750
 
751
+ .a0-composer-actions > .a0-icon-button {
752
+ padding: 0;
753
+ }
754
+
755
+ /* Pause or resume, then stop, as a player's buttons. */
756
+ .a0-player {
757
+ display: flex;
758
+ align-items: center;
759
+ gap: 0.125rem;
760
+ }
761
+
762
+ .a0-composer-actions .a0-player button {
763
+ width: 1.75rem;
764
+ padding: 0;
765
+ color: var(--muted);
766
+ }
767
+
768
+ /* Send, or during a turn Steer or Queue with its mode on the chevron. */
769
+ .a0-send {
770
+ display: flex;
771
+ align-items: center;
772
+ gap: 1px;
773
+ }
774
+
775
+ .a0-composer-actions .a0-send-mode {
776
+ width: 1.5rem;
777
+ padding: 0;
778
+ }
779
+
780
+ .a0-menu-check {
781
+ margin-right: 0.375rem;
782
+ vertical-align: -0.125rem;
783
+ }
784
+
546
785
  .a0-ghost {
547
786
  color: var(--muted);
548
787
  }
@@ -44,11 +44,12 @@ export function AppView({
44
44
  <section aria-label="Chat" className="a0-chat">
45
45
  <h1 className="a0-title">{name}</h1>
46
46
  <Chat
47
+ appId={appId}
47
48
  harness={harness}
48
49
  onPrompt={(text) =>
49
50
  prompted({
50
51
  id: appId,
51
- name: titleFromPrompt(prompt ?? text),
52
+ name: titleFromPrompt(prompt || text),
52
53
  harness,
53
54
  })
54
55
  }
@@ -0,0 +1,133 @@
1
+ 'use client'
2
+ import { upload as uploadToBlob } from '@vercel/blob/client'
3
+ import type { FileUIPart } from 'ai'
4
+ import { useEffect, useRef, useState } from 'react'
5
+ import { isAttachmentUrl } from './model'
6
+
7
+ /**
8
+ * A file the user attached. An image goes out with the message, for the
9
+ * model to see; any other file goes into the message's text as a link, which
10
+ * the agent downloads in its sandbox.
11
+ */
12
+ export type Attachment = {
13
+ id: string
14
+ file: File
15
+ kind: 'image' | 'link'
16
+ /** Once Blob has it. */
17
+ part?: FileUIPart
18
+ }
19
+
20
+ export type Attachments = {
21
+ list: Attachment[]
22
+ add: (files: File[]) => void
23
+ /** Stops its upload, if it's still running. */
24
+ remove: (id: string) => void
25
+ clear: () => void
26
+ /** Puts back what a failed send took, before anything attached since. */
27
+ restore: (sent: Attachment[]) => void
28
+ }
29
+
30
+ const isImage = (file: File): boolean => file.type.startsWith('image/')
31
+
32
+ /** Uploads straight to Blob, with a token from the app's uploads route. */
33
+ async function upload({
34
+ appId,
35
+ file,
36
+ signal,
37
+ }: {
38
+ appId: string
39
+ file: File
40
+ signal: AbortSignal
41
+ }): Promise<FileUIPart> {
42
+ const blob = await uploadToBlob(`a0/${appId}/${file.name}`, file, {
43
+ access: 'public',
44
+ handleUploadUrl: '/a0/uploads',
45
+ clientPayload: appId,
46
+ ...(file.type ? { contentType: file.type } : {}),
47
+ abortSignal: signal,
48
+ }).catch((cause: unknown) => {
49
+ throw new Error(
50
+ `Couldn't upload ${file.name}: ${cause instanceof Error ? cause.message : String(cause)}`,
51
+ { cause },
52
+ )
53
+ })
54
+ return {
55
+ type: 'file',
56
+ mediaType: blob.contentType,
57
+ filename: file.name,
58
+ url: blob.url,
59
+ }
60
+ }
61
+
62
+ /** The line a file that isn't an image adds to the message. */
63
+ const linkLine = (part: FileUIPart): string =>
64
+ `${part.filename ?? 'file'}: ${part.url}`
65
+
66
+ /** One of the composer's link lines, as the file it names. */
67
+ export function attachedLink(
68
+ line: string,
69
+ ): { name: string; url: string } | null {
70
+ const match = /^(.+): (https:\/\/\S+)$/.exec(line)
71
+ return match?.[1] && match[2] && isAttachmentUrl(match[2])
72
+ ? { name: match[1], url: match[2] }
73
+ : null
74
+ }
75
+
76
+ /** The files attached to the next message, each uploading as it's added. */
77
+ export function useAttachments({
78
+ appId,
79
+ onLink,
80
+ onError,
81
+ }: {
82
+ appId: string
83
+ /** A file that isn't an image finished uploading: its line for the text. */
84
+ onLink: (line: string) => void
85
+ onError: (cause: unknown) => void
86
+ }): Attachments {
87
+ const [list, setList] = useState<Attachment[]>([])
88
+ // The uploads still running, which removing their file or leaving the app
89
+ // stops.
90
+ const uploads = useRef(new Map<string, AbortController>())
91
+ useEffect(() => {
92
+ const running = uploads.current
93
+ return () => {
94
+ for (const controller of running.values()) controller.abort()
95
+ }
96
+ }, [])
97
+ const drop = (id: string): void =>
98
+ setList((current) => current.filter((item) => item.id !== id))
99
+ const attach = async (file: File): Promise<void> => {
100
+ const id = crypto.randomUUID()
101
+ const kind = isImage(file) ? 'image' : 'link'
102
+ const controller = new AbortController()
103
+ uploads.current.set(id, controller)
104
+ setList((current) => [...current, { id, file, kind }])
105
+ try {
106
+ const part = await upload({ appId, file, signal: controller.signal })
107
+ if (kind === 'link') {
108
+ drop(id)
109
+ onLink(linkLine(part))
110
+ } else
111
+ setList((current) =>
112
+ current.map((item) => (item.id === id ? { ...item, part } : item)),
113
+ )
114
+ } catch (cause) {
115
+ drop(id)
116
+ if (!controller.signal.aborted) onError(cause)
117
+ } finally {
118
+ uploads.current.delete(id)
119
+ }
120
+ }
121
+ return {
122
+ list,
123
+ add: (files) => {
124
+ for (const file of files) void attach(file)
125
+ },
126
+ remove: (id) => {
127
+ uploads.current.get(id)?.abort()
128
+ drop(id)
129
+ },
130
+ clear: () => setList([]),
131
+ restore: (sent) => setList((current) => [...sent, ...current]),
132
+ }
133
+ }