experimental-a2 0.16.1 → 0.18.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.
- package/CHANGELOG.md +373 -0
- package/README.md +1 -1
- package/dist/{actor-ohPC-81x.d.ts → actor-Bjce9_5U.d.ts} +21 -11
- package/dist/actor-Bjce9_5U.d.ts.map +1 -0
- package/dist/actor-client.d.ts +2 -5
- package/dist/actor-client.d.ts.map +1 -1
- package/dist/actor-client.js +13 -4
- package/dist/actor-client.js.map +1 -1
- package/dist/actor-react.d.ts +5 -3
- package/dist/actor-react.d.ts.map +1 -1
- package/dist/actor-react.js +20 -10
- package/dist/actor-react.js.map +1 -1
- package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
- package/dist/actor-shared-t7w8xIKP.js.map +1 -0
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +42 -23
- package/dist/actor.js.map +1 -1
- package/dist/{ai-DgOBltJ_.js → ai-BpOGm-zB.js} +621 -216
- package/dist/ai-BpOGm-zB.js.map +1 -0
- package/dist/{ai-CrEf6p_W.d.ts → ai-DrUaMyRt.d.ts} +114 -100
- package/dist/ai-DrUaMyRt.d.ts.map +1 -0
- package/dist/ai-client.d.ts +27 -0
- package/dist/ai-client.d.ts.map +1 -0
- package/dist/ai-client.js +29 -0
- package/dist/ai-client.js.map +1 -0
- package/dist/ai-fx.browser.d.ts +1 -0
- package/dist/ai-fx.browser.js +6 -0
- package/dist/ai-fx.browser.js.map +1 -0
- package/dist/ai-fx.d.ts +10 -0
- package/dist/ai-fx.d.ts.map +1 -0
- package/dist/ai-fx.js +634 -0
- package/dist/ai-fx.js.map +1 -0
- package/dist/ai-harness-BuLh_EYi.js +273 -0
- package/dist/ai-harness-BuLh_EYi.js.map +1 -0
- package/dist/ai-harness-DvsPeJeB.d.ts +26 -0
- package/dist/ai-harness-DvsPeJeB.d.ts.map +1 -0
- package/dist/ai-pi.browser.d.ts +1 -0
- package/dist/ai-pi.browser.js +6 -0
- package/dist/ai-pi.browser.js.map +1 -0
- package/dist/ai-pi.d.ts +17 -0
- package/dist/ai-pi.d.ts.map +1 -0
- package/dist/ai-pi.js +904 -0
- package/dist/ai-pi.js.map +1 -0
- package/dist/ai-react.d.ts +96 -0
- package/dist/ai-react.d.ts.map +1 -0
- package/dist/ai-react.js +142 -0
- package/dist/ai-react.js.map +1 -0
- package/dist/ai-server.browser.js +2 -2
- package/dist/ai-server.browser.js.map +1 -1
- package/dist/ai-server.d.ts +109 -43
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +526 -283
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +2 -2
- package/dist/bounded-cache-CPHWF-8h.js +41 -0
- package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
- package/dist/{client-BYzHjkwU.js → client-BReKqwZC.js} +447 -80
- package/dist/client-BReKqwZC.js.map +1 -0
- package/dist/{client-CzyacQpJ.d.ts → client-h_DKFAjj.d.ts} +14 -13
- package/dist/client-h_DKFAjj.d.ts.map +1 -0
- package/dist/client.d.ts +4 -2
- package/dist/client.js +1 -1
- package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
- package/dist/contract-BYKASlCV.js.map +1 -0
- package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
- package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +7 -3
- package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +1 -0
- package/dist/{internal-Dq2qYxou.js → internal-B2T3U4Rq.js} +29 -2
- package/dist/internal-B2T3U4Rq.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/react-BBXgHWFc.d.ts +86 -0
- package/dist/react-BBXgHWFc.d.ts.map +1 -0
- package/dist/react.d.ts +2 -122
- package/dist/react.js +107 -100
- package/dist/react.js.map +1 -1
- package/dist/reducer-BAXc8JyF.js +87 -0
- package/dist/reducer-BAXc8JyF.js.map +1 -0
- package/dist/{reducer-otzHjuJj.d.ts → reducer-CqA59Q6U.d.ts} +46 -30
- package/dist/reducer-CqA59Q6U.d.ts.map +1 -0
- package/dist/reducer-runtime-DQwz_J59.js +28 -0
- package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +3 -3
- package/dist/scheduler-qstash.d.ts.map +1 -1
- package/dist/scheduler-qstash.js +33 -10
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
- package/dist/scheduler-task-X1SToDjP.js.map +1 -0
- package/dist/scheduler-vercel.d.ts +4 -9
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +25 -44
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-BD5ckxZb.js → server-Dw_k_7kO.js} +904 -219
- package/dist/server-Dw_k_7kO.js.map +1 -0
- package/dist/{server-DbD7IJVK.d.ts → server-En7cbi3v.d.ts} +116 -26
- package/dist/server-En7cbi3v.d.ts.map +1 -0
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/session-clear-Bo-XTFEy.d.ts +12 -0
- package/dist/session-clear-Bo-XTFEy.d.ts.map +1 -0
- package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
- package/dist/session-clear-DGI4jTQ5.js.map +1 -0
- package/dist/{store-DyZM6fS5.d.ts → store-Djdv0bnZ.d.ts} +73 -3
- package/dist/store-Djdv0bnZ.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +205 -66
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-CmxUbV93.js → store-polling-B5FcwJfo.js} +21 -4
- package/dist/store-polling-B5FcwJfo.js.map +1 -0
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +450 -108
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-BAwS8RWn.js} +3 -3
- package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-BAwS8RWn.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.d.ts.map +1 -1
- package/dist/store-redis-http.js +293 -11
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis-notify-BRITti_o.js +2493 -0
- package/dist/store-redis-notify-BRITti_o.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.d.ts.map +1 -1
- package/dist/store-redis.js +11 -11
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +197 -33
- package/dist/store-sqlite.js.map +1 -1
- package/dist/stream-buffer-Cx7AAPGq.js +23 -0
- package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
- package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
- package/dist/{wire-BO5wWCb1.js → wire-rSUglYo8.js} +132 -10
- package/dist/wire-rSUglYo8.js.map +1 -0
- package/docs/01-quickstart.mdx +50 -84
- package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
- package/docs/actors/01-introduction.mdx +48 -82
- package/docs/actors/02-concurrency.mdx +43 -70
- package/docs/actors/03-timers.mdx +28 -42
- package/docs/actors/04-routes.mdx +110 -137
- package/docs/actors/meta.ts +1 -1
- package/docs/agents/01-introduction.mdx +257 -0
- package/docs/agents/02-tools.mdx +247 -0
- package/docs/agents/03-conversations.mdx +538 -0
- package/docs/agents/04-harnesses.mdx +136 -0
- package/docs/agents/meta.ts +1 -0
- package/docs/client/01-react.mdx +294 -0
- package/docs/client/02-transports.mdx +86 -0
- package/docs/client/03-presence.mdx +224 -0
- package/docs/client/meta.ts +1 -0
- package/docs/contract/01-events.mdx +57 -0
- package/docs/contract/02-state.mdx +142 -0
- package/docs/contract/03-sessions.mdx +175 -0
- package/docs/index.mdx +112 -156
- package/docs/reference/01-core.mdx +139 -0
- package/docs/reference/02-server.mdx +855 -0
- package/docs/reference/03-client.mdx +301 -0
- package/docs/reference/04-ai.mdx +722 -0
- package/docs/reference/05-adapters.mdx +351 -0
- package/docs/reference/06-errors.mdx +138 -0
- package/docs/reference/meta.ts +1 -1
- package/docs/server/01-handlers.mdx +200 -0
- package/docs/server/02-timers.mdx +130 -0
- package/docs/server/03-cancellation.mdx +141 -0
- package/docs/server/04-routes.mdx +243 -0
- package/docs/server/05-production.mdx +322 -0
- package/docs/server/06-guarantees.mdx +123 -0
- package/examples/playground/DEPLOY.md +53 -7
- package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
- package/examples/playground/app/a0/[appId]/page.tsx +53 -0
- package/examples/playground/app/a0/a0.css +818 -0
- package/examples/playground/app/a0/app-list.tsx +218 -0
- package/examples/playground/app/a0/app-view.tsx +63 -0
- package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
- package/examples/playground/app/a0/apps/[appId]/route.ts +28 -0
- package/examples/playground/app/a0/apps.test.ts +128 -0
- package/examples/playground/app/a0/apps.ts +102 -0
- package/examples/playground/app/a0/chat.tsx +410 -0
- package/examples/playground/app/a0/columns.tsx +109 -0
- package/examples/playground/app/a0/context-menu.tsx +116 -0
- package/examples/playground/app/a0/cookies.ts +46 -0
- package/examples/playground/app/a0/crash-on-request.ts +72 -0
- package/examples/playground/app/a0/crashes/route.ts +3 -0
- package/examples/playground/app/a0/crashes.test.ts +33 -0
- package/examples/playground/app/a0/crashes.ts +28 -0
- package/examples/playground/app/a0/devtools.tsx +164 -0
- package/examples/playground/app/a0/events/route.ts +6 -0
- package/examples/playground/app/a0/flags.ts +30 -0
- package/examples/playground/app/a0/fx/events/route.ts +9 -0
- package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
- package/examples/playground/app/a0/layout.tsx +38 -0
- package/examples/playground/app/a0/list/route.ts +10 -0
- package/examples/playground/app/a0/model.test.ts +73 -0
- package/examples/playground/app/a0/model.ts +95 -0
- package/examples/playground/app/a0/no-photon.cjs +4 -0
- package/examples/playground/app/a0/no-quickjs.cjs +3 -0
- package/examples/playground/app/a0/page.tsx +14 -0
- package/examples/playground/app/a0/pi/events/route.ts +9 -0
- package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
- package/examples/playground/app/a0/preview-pane.tsx +145 -0
- package/examples/playground/app/a0/preview.test.ts +53 -0
- package/examples/playground/app/a0/preview.ts +26 -0
- package/examples/playground/app/a0/sandbox-log.ts +75 -0
- package/examples/playground/app/a0/sandbox-session.ts +152 -0
- package/examples/playground/app/a0/sandbox.ts +191 -0
- package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
- package/examples/playground/app/a0/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/server.ts +213 -0
- package/examples/playground/app/a0/session.ts +59 -0
- package/examples/playground/app/a0/shell.tsx +158 -0
- package/examples/playground/app/a0/short/events/route.ts +9 -0
- package/examples/playground/app/a0/splitter.tsx +120 -0
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
- package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
- package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
- package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
- package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
- package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
- package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
- package/examples/playground/app/agent/events/route.ts +5 -2
- package/examples/playground/app/agent/ingress.test.ts +101 -0
- package/examples/playground/app/agent/model-input.test.tsx +58 -0
- package/examples/playground/app/agent/model-input.tsx +115 -0
- package/examples/playground/app/agent/model.test.ts +73 -0
- package/examples/playground/app/agent/model.ts +58 -26
- package/examples/playground/app/agent/new-agent-session.tsx +37 -19
- package/examples/playground/app/agent/page.tsx +4 -4
- package/examples/playground/app/agent/server.ts +19 -15
- package/examples/playground/app/agent/session.ts +5 -6
- package/examples/playground/app/agent/telemetry.test.ts +189 -0
- package/examples/playground/app/agent/telemetry.ts +30 -0
- package/examples/playground/app/agent/trace.test.ts +23 -0
- package/examples/playground/app/agent/trace.ts +18 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +12 -12
- package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
- package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
- package/examples/playground/app/canvas/model.ts +53 -53
- package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
- package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
- package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
- package/examples/playground/app/chat/[chatId]/session.ts +2 -19
- package/examples/playground/app/chat/agent/route.ts +9 -0
- package/examples/playground/app/chat/model.test.ts +46 -119
- package/examples/playground/app/chat/model.ts +82 -128
- package/examples/playground/app/chat/scheduler/route.ts +2 -2
- package/examples/playground/app/chat/server.test.ts +112 -0
- package/examples/playground/app/chat/server.ts +113 -117
- package/examples/playground/app/components/activity-feed.tsx +30 -1
- package/examples/playground/app/components/activity-history.test.tsx +35 -0
- package/examples/playground/app/components/demo-icons.tsx +89 -0
- package/examples/playground/app/components/demos.tsx +33 -0
- package/examples/playground/app/components/fast-link.test.tsx +58 -0
- package/examples/playground/app/components/fast-link.tsx +55 -0
- package/examples/playground/app/components/site-header.tsx +96 -0
- package/examples/playground/app/counter/counter-client.tsx +3 -3
- package/examples/playground/app/counter/model.test.ts +3 -3
- package/examples/playground/app/counter/model.ts +11 -21
- package/examples/playground/app/counter/page.tsx +2 -15
- package/examples/playground/app/counter/session.ts +2 -2
- package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
- package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
- package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
- package/examples/playground/app/documents/[documentId]/session.ts +2 -2
- package/examples/playground/app/documents/model.ts +27 -27
- package/examples/playground/app/globals.css +420 -207
- package/examples/playground/app/initial-state.test.tsx +54 -0
- package/examples/playground/app/layout.tsx +7 -23
- package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
- package/examples/playground/app/orders/[orderId]/session.ts +2 -2
- package/examples/playground/app/orders/model.ts +49 -49
- package/examples/playground/app/page.tsx +13 -102
- package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
- package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
- package/examples/playground/app/recovery/model.ts +30 -30
- package/examples/playground/instrumentation.ts +5 -0
- package/examples/playground/next.config.ts +17 -5
- package/examples/playground/package.json +13 -2
- package/examples/playground/scripts/a0-template.ts +77 -0
- package/examples/playground/vercel.json +36 -0
- package/package.json +29 -3
- package/src/actor-client.ts +16 -4
- package/src/actor-react.ts +25 -11
- package/src/actor-shared.ts +21 -6
- package/src/actor.ts +76 -31
- package/src/ai-agent-actions.ts +219 -0
- package/src/ai-agent.ts +2517 -0
- package/src/ai-app-state.ts +91 -0
- package/src/ai-client-state.ts +6 -3
- package/src/ai-client.ts +62 -0
- package/src/ai-context-schema.ts +25 -5
- package/src/ai-context.ts +90 -40
- package/src/ai-control-server.ts +92 -72
- package/src/ai-coordinator.ts +23 -22
- package/src/ai-fx.browser.ts +5 -0
- package/src/ai-fx.ts +833 -0
- package/src/ai-harness.ts +518 -0
- package/src/ai-message-base.ts +209 -0
- package/src/ai-message-projection.ts +262 -64
- package/src/ai-model-metadata.ts +11 -6
- package/src/ai-pi.browser.ts +5 -0
- package/src/ai-pi.ts +1263 -0
- package/src/ai-progress-batches.ts +26 -0
- package/src/ai-projector.ts +95 -14
- package/src/ai-react.ts +146 -0
- package/src/ai-sdk-step.ts +2 -2
- package/src/ai-server.browser.ts +2 -2
- package/src/ai-server.ts +23 -1965
- package/src/ai-stored-state.ts +48 -163
- package/src/ai.ts +339 -155
- package/src/bootstrap-stream.ts +72 -0
- package/src/bounded-cache.ts +55 -0
- package/src/client-render-session.ts +49 -0
- package/src/client.ts +688 -151
- package/src/contract.ts +92 -42
- package/src/deterministic-id.ts +37 -0
- package/src/equal-json.ts +32 -0
- package/src/errors.ts +13 -1
- package/src/index.ts +5 -1
- package/src/initial-snapshot.ts +39 -0
- package/src/internal.ts +52 -0
- package/src/libfx.d.ts +34 -0
- package/src/postgres-notification-scope.ts +14 -3
- package/src/react.ts +171 -206
- package/src/redis-http-reply.ts +256 -0
- package/src/redis-http-stream.ts +73 -0
- package/src/redis-http-subscriptions.ts +13 -5
- package/src/reducer-runtime.ts +52 -0
- package/src/reducer.ts +93 -11
- package/src/scheduler-qstash.ts +49 -20
- package/src/scheduler-task.ts +34 -2
- package/src/scheduler-vercel.ts +32 -84
- package/src/server-fetch.ts +139 -35
- package/src/server.ts +1133 -255
- package/src/session-clear.ts +23 -0
- package/src/session-socket.ts +154 -49
- package/src/sse.ts +71 -4
- package/src/store-memory.ts +252 -75
- package/src/store-polling.ts +18 -4
- package/src/store-postgres.ts +541 -114
- package/src/store-redis-core.ts +1450 -493
- package/src/store-redis-http.ts +18 -2
- package/src/store-redis-notify.ts +262 -85
- package/src/store-redis.ts +19 -9
- package/src/store-sqlite.ts +319 -45
- package/src/store.ts +79 -1
- package/src/stream-buffer.ts +43 -0
- package/src/telemetry.ts +14 -1
- package/src/wire.ts +241 -13
- package/dist/actor-ohPC-81x.d.ts.map +0 -1
- package/dist/actor-shared-B5tJfzt-.js.map +0 -1
- package/dist/ai-CrEf6p_W.d.ts.map +0 -1
- package/dist/ai-DgOBltJ_.js.map +0 -1
- package/dist/client-BYzHjkwU.js.map +0 -1
- package/dist/client-CzyacQpJ.d.ts.map +0 -1
- package/dist/contract-CKRg_E4q.js.map +0 -1
- package/dist/errors-DCk6ch5n.js.map +0 -1
- package/dist/errors-DvhSXnxk.d.ts.map +0 -1
- package/dist/internal-Dq2qYxou.js.map +0 -1
- package/dist/react.d.ts.map +0 -1
- package/dist/reducer-DEMjEY_O.js +0 -29
- package/dist/reducer-DEMjEY_O.js.map +0 -1
- package/dist/reducer-otzHjuJj.d.ts.map +0 -1
- package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
- package/dist/server-BD5ckxZb.js.map +0 -1
- package/dist/server-DbD7IJVK.d.ts.map +0 -1
- package/dist/store-DyZM6fS5.d.ts.map +0 -1
- package/dist/store-polling-CmxUbV93.js.map +0 -1
- package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
- package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
- package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
- package/dist/validate-XKT4FSNn.js +0 -30
- package/dist/validate-XKT4FSNn.js.map +0 -1
- package/dist/wire-BO5wWCb1.js.map +0 -1
- package/docs/concepts/01-contracts.mdx +0 -142
- package/docs/concepts/02-handlers.mdx +0 -284
- package/docs/concepts/03-durability.mdx +0 -314
- package/docs/concepts/04-state.mdx +0 -209
- package/docs/guides/01-timers.mdx +0 -185
- package/docs/guides/02-cancellation.mdx +0 -161
- package/docs/guides/03-react.mdx +0 -384
- package/docs/guides/04-local-first.mdx +0 -88
- package/docs/guides/05-production.mdx +0 -521
- package/docs/guides/06-ai-agents.mdx +0 -1318
- package/docs/guides/08-application-data.mdx +0 -113
- package/docs/guides/09-presence.mdx +0 -266
- package/docs/guides/10-transports.mdx +0 -221
- package/docs/reference/01-api.mdx +0 -1896
- package/docs/reference/02-errors.mdx +0 -149
- package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
- package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
- package/examples/playground/app/agent/compaction-settings.ts +0 -49
- package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
- package/examples/playground/app/agent/compaction-timeline.ts +0 -208
- /package/docs/{concepts → contract}/meta.ts +0 -0
- /package/docs/{guides → server}/meta.ts +0 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { useEffect, useRef, useState } from 'react'
|
|
4
|
+
import { useSearchParams } from 'next/navigation'
|
|
5
|
+
import { Asterisk, Plus } from 'lucide-react'
|
|
6
|
+
import { FastLink } from '../components/fast-link'
|
|
7
|
+
import { type App, timeAgo } from './apps'
|
|
8
|
+
import { ContextMenu, type MenuItem } from './context-menu'
|
|
9
|
+
import { queryString } from './flags'
|
|
10
|
+
|
|
11
|
+
/** The clock the list's times read from: the server's at first, then ticking. */
|
|
12
|
+
function useNow(renderedAt: number): number {
|
|
13
|
+
const [now, setNow] = useState(renderedAt)
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const timer = setInterval(() => setNow(Date.now()), 30_000)
|
|
16
|
+
return () => clearInterval(timer)
|
|
17
|
+
}, [])
|
|
18
|
+
return now
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function AppList({
|
|
22
|
+
apps,
|
|
23
|
+
currentId,
|
|
24
|
+
renderedAt,
|
|
25
|
+
newAppId,
|
|
26
|
+
onRename,
|
|
27
|
+
onDelete,
|
|
28
|
+
}: {
|
|
29
|
+
/** Most recently prompted first. */
|
|
30
|
+
apps: App[]
|
|
31
|
+
currentId: string | null
|
|
32
|
+
renderedAt: number
|
|
33
|
+
newAppId: string
|
|
34
|
+
onRename: (app: { id: string; name: string }) => void
|
|
35
|
+
onDelete: (app: App) => void
|
|
36
|
+
}): ReactNode {
|
|
37
|
+
const now = useNow(renderedAt)
|
|
38
|
+
// Links keep the dev tools' state from app to app. A new app starts with
|
|
39
|
+
// the open app's harness; a listed one keeps its own.
|
|
40
|
+
const params = useSearchParams()
|
|
41
|
+
const query = queryString(new URLSearchParams(params))
|
|
42
|
+
const appQuery = (app: App): string => {
|
|
43
|
+
const own = new URLSearchParams(params)
|
|
44
|
+
own.delete('harness')
|
|
45
|
+
if (app.harness) own.set('harness', app.harness)
|
|
46
|
+
return queryString(own)
|
|
47
|
+
}
|
|
48
|
+
// Open dev tools also read each page's log, so they don't prefetch every
|
|
49
|
+
// app's in full.
|
|
50
|
+
const prefetch = params.has('devtools') ? null : true
|
|
51
|
+
return (
|
|
52
|
+
<>
|
|
53
|
+
<div className="a0-label">
|
|
54
|
+
<span>Apps</span>
|
|
55
|
+
<span className="a0-rule" />
|
|
56
|
+
<FastLink
|
|
57
|
+
href={`/a0/${newAppId}${query}`}
|
|
58
|
+
prefetch={prefetch}
|
|
59
|
+
className="a0-icon-button"
|
|
60
|
+
aria-label="New app"
|
|
61
|
+
title="New app"
|
|
62
|
+
>
|
|
63
|
+
<Plus size={14} />
|
|
64
|
+
</FastLink>
|
|
65
|
+
</div>
|
|
66
|
+
<nav aria-label="Apps">
|
|
67
|
+
{apps.map((app) => (
|
|
68
|
+
<AppItem
|
|
69
|
+
key={app.id}
|
|
70
|
+
app={app}
|
|
71
|
+
when={timeAgo({ at: app.promptedAt, now })}
|
|
72
|
+
current={app.id === currentId}
|
|
73
|
+
href={`/a0/${app.id}${appQuery(app)}`}
|
|
74
|
+
prefetch={prefetch}
|
|
75
|
+
onRename={(name) => onRename({ id: app.id, name })}
|
|
76
|
+
onDelete={() => onDelete(app)}
|
|
77
|
+
/>
|
|
78
|
+
))}
|
|
79
|
+
</nav>
|
|
80
|
+
</>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const under = (row: HTMLElement): { x: number; y: number } => {
|
|
85
|
+
const box = row.getBoundingClientRect()
|
|
86
|
+
return { x: box.left + 16, y: box.bottom }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Clicking the current app renames it in place. Right-clicking any app, or
|
|
91
|
+
* its context-menu key, opens its menu.
|
|
92
|
+
*/
|
|
93
|
+
function AppItem({
|
|
94
|
+
app,
|
|
95
|
+
when,
|
|
96
|
+
current,
|
|
97
|
+
href,
|
|
98
|
+
prefetch,
|
|
99
|
+
onRename,
|
|
100
|
+
onDelete,
|
|
101
|
+
}: {
|
|
102
|
+
app: App
|
|
103
|
+
when: string
|
|
104
|
+
current: boolean
|
|
105
|
+
href: string
|
|
106
|
+
prefetch: boolean | null
|
|
107
|
+
onRename: (name: string) => void
|
|
108
|
+
onDelete: () => void
|
|
109
|
+
}): ReactNode {
|
|
110
|
+
const [editing, setEditing] = useState(false)
|
|
111
|
+
const [menu, setMenu] = useState<{ x: number; y: number } | null>(null)
|
|
112
|
+
const row = useRef<HTMLDivElement>(null)
|
|
113
|
+
const cancelled = useRef(false)
|
|
114
|
+
const rename = (): void => {
|
|
115
|
+
cancelled.current = false
|
|
116
|
+
setEditing(true)
|
|
117
|
+
}
|
|
118
|
+
const link = (): string => new URL(href, location.origin).href
|
|
119
|
+
const items: MenuItem[] = [
|
|
120
|
+
{ label: 'Open in new tab', onSelect: () => window.open(link(), '_blank') },
|
|
121
|
+
{
|
|
122
|
+
label: 'Copy link',
|
|
123
|
+
onSelect: () => void navigator.clipboard.writeText(link()),
|
|
124
|
+
},
|
|
125
|
+
{ label: 'Rename', onSelect: rename },
|
|
126
|
+
{ label: 'Delete', onSelect: onDelete, danger: true },
|
|
127
|
+
]
|
|
128
|
+
const icon = <Asterisk size={14} aria-hidden />
|
|
129
|
+
const lines = (
|
|
130
|
+
<span className="a0-app-lines">
|
|
131
|
+
<span className="a0-app-name">{app.name}</span>
|
|
132
|
+
<span className="a0-app-when">{when}</span>
|
|
133
|
+
</span>
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if (editing)
|
|
137
|
+
return (
|
|
138
|
+
<div className="a0-app" data-current>
|
|
139
|
+
{icon}
|
|
140
|
+
<input
|
|
141
|
+
aria-label={`Rename ${app.name}`}
|
|
142
|
+
defaultValue={app.name}
|
|
143
|
+
maxLength={80}
|
|
144
|
+
autoFocus
|
|
145
|
+
onFocus={(event) => event.currentTarget.select()}
|
|
146
|
+
onBlur={(event) => {
|
|
147
|
+
setEditing(false)
|
|
148
|
+
const next = event.currentTarget.value.trim()
|
|
149
|
+
if (!cancelled.current && next && next !== app.name) onRename(next)
|
|
150
|
+
}}
|
|
151
|
+
onKeyDown={(event) => {
|
|
152
|
+
if (event.key === 'Enter') event.currentTarget.blur()
|
|
153
|
+
if (event.key === 'Escape') {
|
|
154
|
+
cancelled.current = true
|
|
155
|
+
event.currentTarget.blur()
|
|
156
|
+
}
|
|
157
|
+
}}
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div
|
|
164
|
+
ref={row}
|
|
165
|
+
className="a0-app"
|
|
166
|
+
data-current={current || undefined}
|
|
167
|
+
data-menu={menu ? '' : undefined}
|
|
168
|
+
onContextMenu={(event) => {
|
|
169
|
+
event.preventDefault()
|
|
170
|
+
// The keyboard's menu key has no pointer, so it opens under the row.
|
|
171
|
+
setMenu(
|
|
172
|
+
event.clientX || event.clientY
|
|
173
|
+
? { x: event.clientX, y: event.clientY }
|
|
174
|
+
: under(event.currentTarget),
|
|
175
|
+
)
|
|
176
|
+
}}
|
|
177
|
+
// Not every browser turns these keys into a contextmenu event.
|
|
178
|
+
onKeyDown={(event) => {
|
|
179
|
+
if (
|
|
180
|
+
event.key !== 'ContextMenu' &&
|
|
181
|
+
!(event.key === 'F10' && event.shiftKey)
|
|
182
|
+
)
|
|
183
|
+
return
|
|
184
|
+
event.preventDefault()
|
|
185
|
+
setMenu(under(event.currentTarget))
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
{current ? (
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
className="a0-app-open"
|
|
192
|
+
aria-current="page"
|
|
193
|
+
title="Rename"
|
|
194
|
+
onClick={rename}
|
|
195
|
+
>
|
|
196
|
+
{icon}
|
|
197
|
+
{lines}
|
|
198
|
+
</button>
|
|
199
|
+
) : (
|
|
200
|
+
<FastLink className="a0-app-open" href={href} prefetch={prefetch}>
|
|
201
|
+
{icon}
|
|
202
|
+
{lines}
|
|
203
|
+
</FastLink>
|
|
204
|
+
)}
|
|
205
|
+
{menu && (
|
|
206
|
+
<ContextMenu
|
|
207
|
+
at={menu}
|
|
208
|
+
items={items}
|
|
209
|
+
onClose={({ returnFocus }) => {
|
|
210
|
+
setMenu(null)
|
|
211
|
+
if (returnFocus)
|
|
212
|
+
row.current?.querySelector<HTMLElement>('.a0-app-open')?.focus()
|
|
213
|
+
}}
|
|
214
|
+
/>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { useEffect } from 'react'
|
|
4
|
+
import { useSearchParams } from 'next/navigation'
|
|
5
|
+
import { titleFromPrompt } from './apps'
|
|
6
|
+
import { Chat } from './chat'
|
|
7
|
+
import { ColumnEdge } from './columns'
|
|
8
|
+
import type { Harness } from './model'
|
|
9
|
+
import { checkApp, PreviewPane } from './preview-pane'
|
|
10
|
+
import { useSession } from './session'
|
|
11
|
+
import { useApps } from './shell'
|
|
12
|
+
|
|
13
|
+
/** One app: its chat with the agent, and the app it builds. */
|
|
14
|
+
export function AppView({
|
|
15
|
+
appId,
|
|
16
|
+
harness,
|
|
17
|
+
devtools,
|
|
18
|
+
}: {
|
|
19
|
+
appId: string
|
|
20
|
+
harness: Harness
|
|
21
|
+
/** Shown while the query has `devtools`. */
|
|
22
|
+
devtools: ReactNode
|
|
23
|
+
}): ReactNode {
|
|
24
|
+
const { listed, prompted, opened } = useApps()
|
|
25
|
+
const { state } = useSession()
|
|
26
|
+
const showDevtools = useSearchParams().has('devtools')
|
|
27
|
+
const app = listed(appId)
|
|
28
|
+
const prompt = state.messages
|
|
29
|
+
.find((message) => message.role === 'user')
|
|
30
|
+
?.parts.flatMap((part) => (part.type === 'text' ? [part.text] : []))
|
|
31
|
+
.join(' ')
|
|
32
|
+
const name = app?.name ?? (prompt ? titleFromPrompt(prompt) : 'New app')
|
|
33
|
+
// A new app's sandbox opens on sight, so it's ready by the first prompt;
|
|
34
|
+
// if none comes, its idle check deletes it.
|
|
35
|
+
const newApp = state.messages.length === 0
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!newApp) return
|
|
38
|
+
opened(appId)
|
|
39
|
+
checkApp({ appId, newApp }).catch(() => {})
|
|
40
|
+
}, [appId])
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
<section aria-label="Chat" className="a0-chat">
|
|
45
|
+
<h1 className="a0-title">{name}</h1>
|
|
46
|
+
<Chat
|
|
47
|
+
harness={harness}
|
|
48
|
+
onPrompt={(text) =>
|
|
49
|
+
prompted({
|
|
50
|
+
id: appId,
|
|
51
|
+
name: titleFromPrompt(prompt ?? text),
|
|
52
|
+
harness,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
autoFocus={!app}
|
|
56
|
+
/>
|
|
57
|
+
<ColumnEdge column="chat" />
|
|
58
|
+
</section>
|
|
59
|
+
<PreviewPane appId={appId} />
|
|
60
|
+
{showDevtools && devtools}
|
|
61
|
+
</>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { isAppId } from '../../../apps'
|
|
3
|
+
import { ensureApp } from '../../../sandbox'
|
|
4
|
+
import { checkWhenIdle } from '../../../sandbox-log'
|
|
5
|
+
|
|
6
|
+
const body = z.object({ newApp: z.boolean().default(false) })
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Makes sure the app is up, and says whether it answers. The browser calls
|
|
10
|
+
* it while the app is on screen, which keeps its sandbox running. A new
|
|
11
|
+
* app's sandbox also gets its idle check; the check itself decides, so a
|
|
12
|
+
* wrong `newApp` costs nothing.
|
|
13
|
+
*/
|
|
14
|
+
export async function POST(
|
|
15
|
+
request: Request,
|
|
16
|
+
context: { params: Promise<{ appId: string }> },
|
|
17
|
+
): Promise<Response> {
|
|
18
|
+
const { appId } = await context.params
|
|
19
|
+
if (!isAppId(appId))
|
|
20
|
+
return Response.json({ error: 'Unknown app.' }, { status: 404 })
|
|
21
|
+
const { newApp } = body
|
|
22
|
+
.catch({ newApp: false })
|
|
23
|
+
.parse(await request.json().catch(() => ({})))
|
|
24
|
+
const { up, createdAt } = await ensureApp({ appId })
|
|
25
|
+
if (newApp) await checkWhenIdle({ appId, createdAt })
|
|
26
|
+
return Response.json({ up }, { headers: { 'cache-control': 'no-store' } })
|
|
27
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { inputs } from 'experimental-a2/ai'
|
|
2
|
+
import { isAppId } from '../../apps'
|
|
3
|
+
import { deleteAppSandbox } from '../../sandbox'
|
|
4
|
+
import { agentServers } from '../../server'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Stops the agent's current turn, so no later tool call brings the sandbox
|
|
8
|
+
* back, then deletes it. The conversation stays in the A2 log.
|
|
9
|
+
*/
|
|
10
|
+
export async function DELETE(
|
|
11
|
+
_request: Request,
|
|
12
|
+
context: { params: Promise<{ appId: string }> },
|
|
13
|
+
): Promise<Response> {
|
|
14
|
+
const { appId } = await context.params
|
|
15
|
+
if (!isAppId(appId))
|
|
16
|
+
return Response.json({ error: 'Unknown app.' }, { status: 404 })
|
|
17
|
+
// The app's agent is whichever harness's agent has its log.
|
|
18
|
+
await Promise.all(
|
|
19
|
+
Object.values(agentServers).map(async (server) => {
|
|
20
|
+
const session = server.session(appId)
|
|
21
|
+
const { state } = await session.state()
|
|
22
|
+
if (state.active)
|
|
23
|
+
await session.append(...inputs.stop({ turnId: state.active.turnId }))
|
|
24
|
+
}),
|
|
25
|
+
)
|
|
26
|
+
await deleteAppSandbox({ appId })
|
|
27
|
+
return Response.json({ ok: true })
|
|
28
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { ContractEvent } from 'experimental-a2'
|
|
2
|
+
import { expect, it } from 'vitest'
|
|
3
|
+
import {
|
|
4
|
+
type App,
|
|
5
|
+
appList,
|
|
6
|
+
byLatestPrompt,
|
|
7
|
+
isAppId,
|
|
8
|
+
timeAgo,
|
|
9
|
+
titleFromPrompt,
|
|
10
|
+
} from './apps'
|
|
11
|
+
import { parseWidths, serializeWidths } from './cookies'
|
|
12
|
+
|
|
13
|
+
type ListEvents = typeof appList.events
|
|
14
|
+
|
|
15
|
+
const first = '6f1c1c3e-2f7a-4f0e-9d4e-5b8a9f1d2c3b'
|
|
16
|
+
const second = '0d9a8b7c-6e5f-4a3b-8c2d-1e0f9a8b7c6d'
|
|
17
|
+
|
|
18
|
+
const fold = (
|
|
19
|
+
...events: [type: keyof ListEvents & string, payload: object, at: number][]
|
|
20
|
+
): App[] =>
|
|
21
|
+
events.reduce(
|
|
22
|
+
(apps, [type, payload, at], index) =>
|
|
23
|
+
appList.reducer.fold(apps, {
|
|
24
|
+
id: `event-${index}`,
|
|
25
|
+
type,
|
|
26
|
+
payload,
|
|
27
|
+
index: index + 1,
|
|
28
|
+
sessionId: 'list-1',
|
|
29
|
+
createdAt: new Date(at),
|
|
30
|
+
} as ContractEvent<ListEvents>),
|
|
31
|
+
appList.reducer.initialState,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
it('lists apps with the server time of their latest prompt', () => {
|
|
35
|
+
expect(
|
|
36
|
+
fold(
|
|
37
|
+
['app.created', { appId: first, name: 'todo app' }, 1_000],
|
|
38
|
+
['app.created', { appId: second, name: 'counter' }, 2_000],
|
|
39
|
+
['app.prompted', { appId: first }, 3_000],
|
|
40
|
+
),
|
|
41
|
+
).toEqual([
|
|
42
|
+
{ id: first, name: 'todo app', promptedAt: 3_000 },
|
|
43
|
+
{ id: second, name: 'counter', promptedAt: 2_000 },
|
|
44
|
+
])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('renames and deletes apps, and lists each one once', () => {
|
|
48
|
+
expect(
|
|
49
|
+
fold(
|
|
50
|
+
['app.created', { appId: first, name: 'todo app' }, 1_000],
|
|
51
|
+
['app.created', { appId: first, name: 'again' }, 2_000],
|
|
52
|
+
['app.renamed', { appId: first, name: 'Tasks' }, 3_000],
|
|
53
|
+
['app.created', { appId: second, name: 'counter' }, 4_000],
|
|
54
|
+
['app.deleted', { appId: second }, 5_000],
|
|
55
|
+
),
|
|
56
|
+
).toEqual([{ id: first, name: 'Tasks', promptedAt: 1_000 }])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('sorts by the latest prompt first', () => {
|
|
60
|
+
const apps = [
|
|
61
|
+
{ id: first, name: 'older', promptedAt: 1_000 },
|
|
62
|
+
{ id: second, name: 'newer', promptedAt: 2_000 },
|
|
63
|
+
]
|
|
64
|
+
expect(byLatestPrompt(apps).map((app) => app.name)).toEqual([
|
|
65
|
+
'newer',
|
|
66
|
+
'older',
|
|
67
|
+
])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('rejects names and ids the list would not show', () => {
|
|
71
|
+
const created = appList.events['app.created']
|
|
72
|
+
const valid = (payload: object): boolean =>
|
|
73
|
+
!('issues' in created['~standard'].validate(payload))
|
|
74
|
+
expect(valid({ appId: first, name: 'todo app' })).toBe(true)
|
|
75
|
+
expect(valid({ appId: first, name: ' ' })).toBe(false)
|
|
76
|
+
expect(valid({ appId: first, name: 'x'.repeat(81) })).toBe(false)
|
|
77
|
+
expect(valid({ appId: 'not-a-uuid', name: 'todo app' })).toBe(false)
|
|
78
|
+
expect(isAppId(first)).toBe(true)
|
|
79
|
+
expect(isAppId('not-a-uuid')).toBe(false)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('names an app after the first line of its prompt, cut to fit', () => {
|
|
83
|
+
expect(titleFromPrompt(' make a todo app\nwith tags ')).toBe(
|
|
84
|
+
'make a todo app',
|
|
85
|
+
)
|
|
86
|
+
const long = titleFromPrompt(
|
|
87
|
+
'build a landing page for a coffee shop called Ember with a menu',
|
|
88
|
+
)
|
|
89
|
+
expect(long).toBe('build a landing page for a coffee shop…')
|
|
90
|
+
expect(long.length).toBeLessThanOrEqual(40)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('says how long ago in words', () => {
|
|
94
|
+
const now = Date.UTC(2026, 8, 26, 12)
|
|
95
|
+
const ago = (ms: number): string => timeAgo({ at: now - ms, now })
|
|
96
|
+
expect(ago(-5_000)).toBe('just now')
|
|
97
|
+
expect(ago(30_000)).toBe('just now')
|
|
98
|
+
expect(ago(60_000)).toBe('1 minute ago')
|
|
99
|
+
expect(ago(5 * 60_000)).toBe('5 minutes ago')
|
|
100
|
+
expect(ago(3 * 60 * 60_000)).toBe('3 hours ago')
|
|
101
|
+
expect(ago(30 * 60 * 60_000)).toBe('yesterday')
|
|
102
|
+
expect(ago(10 * 24 * 60 * 60_000)).toBe('last week')
|
|
103
|
+
expect(ago(400 * 24 * 60 * 60_000)).toBe('last year')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('reads the widths someone dragged to, and ignores bad ones', () => {
|
|
107
|
+
expect(parseWidths(undefined)).toEqual({ apps: 216, chat: null })
|
|
108
|
+
expect(parseWidths('240,520')).toEqual({ apps: 240, chat: 520 })
|
|
109
|
+
expect(parseWidths('240')).toEqual({ apps: 240, chat: null })
|
|
110
|
+
expect(parseWidths('9999,10')).toEqual({ apps: 216, chat: null })
|
|
111
|
+
expect(parseWidths('abc,def')).toEqual({ apps: 216, chat: null })
|
|
112
|
+
expect(parseWidths(serializeWidths({ apps: 200, chat: 480 }))).toEqual({
|
|
113
|
+
apps: 200,
|
|
114
|
+
chat: 480,
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("keeps the harness an app's agent runs, for its link", () => {
|
|
119
|
+
expect(
|
|
120
|
+
fold(
|
|
121
|
+
['app.created', { appId: first, name: 'todo app', harness: 'fx' }, 1_000],
|
|
122
|
+
['app.created', { appId: second, name: 'counter' }, 2_000],
|
|
123
|
+
),
|
|
124
|
+
).toEqual([
|
|
125
|
+
{ id: first, name: 'todo app', promptedAt: 1_000, harness: 'fx' },
|
|
126
|
+
{ id: second, name: 'counter', promptedAt: 2_000 },
|
|
127
|
+
])
|
|
128
|
+
})
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { contract } from 'experimental-a2'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { type Harness, harnessSchema } from './model'
|
|
4
|
+
|
|
5
|
+
export const isAppId = (value: string): boolean =>
|
|
6
|
+
z.uuid().safeParse(value).success
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* An app is one conversation with the coding agent and one sandbox. It's
|
|
10
|
+
* listed with the time of its latest prompt, as the server recorded it, and
|
|
11
|
+
* the harness its agent runs when that's not the AI SDK's.
|
|
12
|
+
*/
|
|
13
|
+
export type App = {
|
|
14
|
+
id: string
|
|
15
|
+
name: string
|
|
16
|
+
promptedAt: number
|
|
17
|
+
harness?: Harness
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const appId = z.uuid()
|
|
21
|
+
const name = z.string().trim().min(1).max(80)
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* One browser's apps. Each app is a session of its own; this one lists the
|
|
25
|
+
* apps this browser has prompted, since knowing an app's id is what lets you
|
|
26
|
+
* open it.
|
|
27
|
+
*/
|
|
28
|
+
export const appList = contract({
|
|
29
|
+
name: 'a0-apps',
|
|
30
|
+
events: {
|
|
31
|
+
'app.created': z.object({
|
|
32
|
+
appId,
|
|
33
|
+
name,
|
|
34
|
+
harness: harnessSchema.optional(),
|
|
35
|
+
}),
|
|
36
|
+
'app.prompted': z.object({ appId }),
|
|
37
|
+
'app.renamed': z.object({ appId, name }),
|
|
38
|
+
'app.deleted': z.object({ appId }),
|
|
39
|
+
},
|
|
40
|
+
reducer: {
|
|
41
|
+
name: 'a0-apps.v1',
|
|
42
|
+
initialState: [] as App[],
|
|
43
|
+
fold: (apps, event): App[] => {
|
|
44
|
+
const { appId: id } = event.payload
|
|
45
|
+
const promptedAt = event.createdAt.getTime()
|
|
46
|
+
switch (event.type) {
|
|
47
|
+
case 'app.created': {
|
|
48
|
+
if (apps.some((app) => app.id === id)) return apps
|
|
49
|
+
const { name: created, harness } = event.payload
|
|
50
|
+
return [
|
|
51
|
+
...apps,
|
|
52
|
+
{ id, name: created, promptedAt, ...(harness ? { harness } : {}) },
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
case 'app.prompted':
|
|
56
|
+
return apps.map((app) =>
|
|
57
|
+
app.id === id ? { ...app, promptedAt } : app,
|
|
58
|
+
)
|
|
59
|
+
case 'app.renamed': {
|
|
60
|
+
const { name: renamed } = event.payload
|
|
61
|
+
return apps.map((app) =>
|
|
62
|
+
app.id === id ? { ...app, name: renamed } : app,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
case 'app.deleted':
|
|
66
|
+
return apps.filter((app) => app.id !== id)
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
export const byLatestPrompt = (apps: readonly App[]): App[] =>
|
|
73
|
+
apps.toSorted((a, b) => b.promptedAt - a.promptedAt)
|
|
74
|
+
|
|
75
|
+
const maxTitle = 40
|
|
76
|
+
|
|
77
|
+
/** An app's name from its prompt: the first line, cut to fit. */
|
|
78
|
+
export function titleFromPrompt(prompt: string): string {
|
|
79
|
+
const line = (prompt.trim().split('\n')[0] ?? '').replace(/\s+/g, ' ')
|
|
80
|
+
return line.length > maxTitle
|
|
81
|
+
? `${line.slice(0, maxTitle - 1).trimEnd()}…`
|
|
82
|
+
: line
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const relative = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
|
|
86
|
+
const units = [
|
|
87
|
+
['year', 365 * 24 * 60 * 60_000],
|
|
88
|
+
['month', 30 * 24 * 60 * 60_000],
|
|
89
|
+
['week', 7 * 24 * 60 * 60_000],
|
|
90
|
+
['day', 24 * 60 * 60_000],
|
|
91
|
+
['hour', 60 * 60_000],
|
|
92
|
+
['minute', 60_000],
|
|
93
|
+
] as const
|
|
94
|
+
|
|
95
|
+
/** How long ago, in words: "just now", "5 minutes ago", "yesterday". */
|
|
96
|
+
export function timeAgo({ at, now }: { at: number; now: number }): string {
|
|
97
|
+
const elapsed = now - at
|
|
98
|
+
for (const [unit, size] of units)
|
|
99
|
+
if (elapsed >= size)
|
|
100
|
+
return relative.format(-Math.floor(elapsed / size), unit)
|
|
101
|
+
return 'just now'
|
|
102
|
+
}
|