experimental-a2 0.21.1 → 0.22.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 (139) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/{actor-HnGmrOZZ.d.ts → actor-CqibLgjQ.d.ts} +13 -5
  3. package/dist/actor-CqibLgjQ.d.ts.map +1 -0
  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-shared-DFET5q16.js.map +1 -1
  8. package/dist/actor.d.ts +1 -1
  9. package/dist/actor.js +3 -3
  10. package/dist/{ai-CvEnqVfo.d.ts → ai-DAu77CDr.d.ts} +2 -2
  11. package/dist/{ai-CvEnqVfo.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
  12. package/dist/{ai-CzE672Q3.js → ai-EEfKyc-z.js} +2 -2
  13. package/dist/{ai-CzE672Q3.js.map → ai-EEfKyc-z.js.map} +1 -1
  14. package/dist/ai-client.d.ts +1 -1
  15. package/dist/ai-client.js +2 -2
  16. package/dist/ai-fx.d.ts +4 -2
  17. package/dist/ai-fx.d.ts.map +1 -1
  18. package/dist/ai-fx.js +211 -61
  19. package/dist/ai-fx.js.map +1 -1
  20. package/dist/{ai-harness-Bfk-Lycn.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
  21. package/dist/{ai-harness-Bfk-Lycn.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
  22. package/dist/ai-harness-dQeJaR1t.js.map +1 -1
  23. package/dist/ai-pi.d.ts +1 -1
  24. package/dist/ai-pi.js +1 -1
  25. package/dist/ai-react.d.ts +1 -1
  26. package/dist/ai-react.js +2 -2
  27. package/dist/ai-server.d.ts +3 -3
  28. package/dist/ai-server.d.ts.map +1 -1
  29. package/dist/ai-server.js +10 -4
  30. package/dist/ai-server.js.map +1 -1
  31. package/dist/ai.d.ts +1 -1
  32. package/dist/ai.js +1 -1
  33. package/dist/{client-9IeTsiAs.js → client-BQS4i_V9.js} +27 -4
  34. package/dist/client-BQS4i_V9.js.map +1 -0
  35. package/dist/client-CUMbS_38.d.ts.map +1 -1
  36. package/dist/client.js +1 -1
  37. package/dist/index.d.ts +2 -2
  38. package/dist/{internal-Bc4PvfHk.js → internal-CvsrprnU.js} +32 -15
  39. package/dist/{internal-Bc4PvfHk.js.map → internal-CvsrprnU.js.map} +1 -1
  40. package/dist/react.js +1 -1
  41. package/dist/{runtime-consumer-BWmht60z.js → runtime-consumer-CIeL33wj.js} +2 -2
  42. package/dist/{runtime-consumer-BWmht60z.js.map → runtime-consumer-CIeL33wj.js.map} +1 -1
  43. package/dist/scheduler-qstash.d.ts +2 -2
  44. package/dist/scheduler-qstash.js +2 -2
  45. package/dist/scheduler-vercel.d.ts +2 -2
  46. package/dist/scheduler-vercel.js +2 -2
  47. package/dist/{server-DvIB8kj5.js → server-DtFrWxi3.js} +30 -8
  48. package/dist/server-DtFrWxi3.js.map +1 -0
  49. package/dist/{server-BshFZ0eL.d.ts → server-qgez0Xny.d.ts} +3 -3
  50. package/dist/{server-BshFZ0eL.d.ts.map → server-qgez0Xny.d.ts.map} +1 -1
  51. package/dist/server.d.ts +2 -2
  52. package/dist/server.js +1 -1
  53. package/dist/{session-clear-Bk_29eEw.d.ts → session-clear-D1PFe7xl.d.ts} +2 -2
  54. package/dist/session-clear-D1PFe7xl.d.ts.map +1 -0
  55. package/dist/{store-Z2qjw50n.d.ts → store-D0MhBvg6.d.ts} +15 -6
  56. package/dist/store-D0MhBvg6.d.ts.map +1 -0
  57. package/dist/store-N8PXxDAS.js.map +1 -1
  58. package/dist/store-memory.d.ts +1 -1
  59. package/dist/store-memory.d.ts.map +1 -1
  60. package/dist/store-memory.js +15 -17
  61. package/dist/store-memory.js.map +1 -1
  62. package/dist/{store-polling-DYNyxCev.js → store-polling-BZt2Rf3D.js} +2 -2
  63. package/dist/{store-polling-DYNyxCev.js.map → store-polling-BZt2Rf3D.js.map} +1 -1
  64. package/dist/store-postgres.d.ts +1 -1
  65. package/dist/store-postgres.d.ts.map +1 -1
  66. package/dist/store-postgres.js +31 -40
  67. package/dist/store-postgres.js.map +1 -1
  68. package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
  69. package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
  70. package/dist/store-redis-http.d.ts +1 -1
  71. package/dist/store-redis-http.js +3 -3
  72. package/dist/{store-redis-notify-IiVdlYHc.js → store-redis-notify-CsJw_hfD.js} +538 -397
  73. package/dist/store-redis-notify-CsJw_hfD.js.map +1 -0
  74. package/dist/store-redis.d.ts +1 -1
  75. package/dist/store-redis.d.ts.map +1 -1
  76. package/dist/store-redis.js +3 -4
  77. package/dist/store-redis.js.map +1 -1
  78. package/dist/store-sqlite.d.ts +1 -1
  79. package/dist/store-sqlite.d.ts.map +1 -1
  80. package/dist/store-sqlite.js +16 -18
  81. package/dist/store-sqlite.js.map +1 -1
  82. package/dist/stream-buffer-DrTyiIuo.js +57 -0
  83. package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
  84. package/dist/{wire-CZGqNZRx.js → wire-CV-Z7wz-.js} +3 -3
  85. package/dist/{wire-CZGqNZRx.js.map → wire-CV-Z7wz-.js.map} +1 -1
  86. package/docs/actors/01-introduction.mdx +5 -2
  87. package/docs/agents/03-conversations.mdx +9 -8
  88. package/docs/agents/04-harnesses.mdx +7 -4
  89. package/docs/reference/01-core.mdx +3 -2
  90. package/docs/reference/02-server.mdx +5 -1
  91. package/docs/reference/04-ai.mdx +13 -4
  92. package/docs/reference/06-errors.mdx +1 -1
  93. package/docs/server/01-consumers.mdx +4 -1
  94. package/docs/server/06-guarantees.mdx +7 -4
  95. package/docs/stores/02-redis.mdx +16 -10
  96. package/docs/stores/03-redis-http.mdx +6 -4
  97. package/docs/stores/04-postgres.mdx +2 -2
  98. package/docs/upgrades/01-overview.mdx +1 -0
  99. package/docs/upgrades/78-0-22.mdx +115 -0
  100. package/examples/playground/app/a0/sandbox.ts +58 -13
  101. package/examples/playground/app/a2slack/db/a2.ts +7 -4
  102. package/examples/playground/app/a2slack/db/migrations/0004_a2_session_event_ids.sql +2 -0
  103. package/examples/playground/app/a2slack/db/migrations/meta/0004_snapshot.json +1165 -0
  104. package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +8 -1
  105. package/examples/playground/app/chat/server.ts +2 -2
  106. package/examples/playground/lib/store.ts +1 -1
  107. package/examples/playground/package.json +1 -1
  108. package/package.json +1 -1
  109. package/sql/postgres/drizzle-schema.ts +7 -4
  110. package/sql/postgres/schema.sql +24 -3
  111. package/src/actor-shared.ts +10 -2
  112. package/src/ai-agent.ts +13 -0
  113. package/src/ai-fx.ts +321 -77
  114. package/src/ai-harness.ts +7 -0
  115. package/src/client.ts +19 -2
  116. package/src/deterministic-id.ts +1 -1
  117. package/src/internal.ts +42 -11
  118. package/src/presence-recovery.ts +18 -11
  119. package/src/presence-tombstones.ts +52 -0
  120. package/src/push-envelope.ts +2 -2
  121. package/src/server.ts +49 -15
  122. package/src/store-memory.ts +24 -32
  123. package/src/store-postgres.ts +43 -53
  124. package/src/store-presence-polling.ts +59 -34
  125. package/src/store-redis-core.ts +595 -439
  126. package/src/store-redis-notify.ts +74 -26
  127. package/src/store-redis.ts +2 -3
  128. package/src/store-sqlite.ts +34 -40
  129. package/src/store.ts +14 -5
  130. package/dist/actor-HnGmrOZZ.d.ts.map +0 -1
  131. package/dist/client-9IeTsiAs.js.map +0 -1
  132. package/dist/server-DvIB8kj5.js.map +0 -1
  133. package/dist/session-clear-Bk_29eEw.d.ts.map +0 -1
  134. package/dist/store-Z2qjw50n.d.ts.map +0 -1
  135. package/dist/store-presence-polling-CzQ0wdtM.js +0 -94
  136. package/dist/store-presence-polling-CzQ0wdtM.js.map +0 -1
  137. package/dist/store-redis-notify-IiVdlYHc.js.map +0 -1
  138. package/dist/stream-buffer-Cx7AAPGq.js +0 -23
  139. package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
@@ -68,6 +68,7 @@ async function openApp(appId: string): Promise<Sandbox> {
68
68
  persistent: true,
69
69
  snapshotExpiration: SNAPSHOT_EXPIRATION_MS,
70
70
  keepLastSnapshots: { count: 1 },
71
+ onResume: startDev,
71
72
  })
72
73
  } catch (error) {
73
74
  // Another instance created it first. The API holds a racing create until
@@ -81,8 +82,54 @@ async function openApp(appId: string): Promise<Sandbox> {
81
82
  return sandbox
82
83
  }
83
84
 
84
- // Parallel tool calls on a new app share one create within this instance.
85
- const opening = new Map<string, Promise<Sandbox>>()
85
+ // Each instance keeps one handle per app, so parallel tool calls share an
86
+ // open and later ones skip the lookup. A handle resumes a stopped session on
87
+ // its next call; a deleted sandbox fails with a 404 until it's forgotten.
88
+ // An empty app's idle check deletes its sandbox from whichever instance runs
89
+ // it, 10 minutes after the sandbox opened. By then the session's 15 minutes
90
+ // leave less than KEEP_ALIVE_MS, so every call extends it, and the extension's
91
+ // 404 is how a kept handle finds out.
92
+ const handles = new Map<string, Promise<Sandbox>>()
93
+ const MAX_HANDLES = 100
94
+
95
+ const forget = (appId: string): void => {
96
+ handles.delete(appId)
97
+ }
98
+
99
+ const open = (appId: string): Promise<Sandbox> => {
100
+ const opened = openApp(appId)
101
+ opened.catch(() => {
102
+ if (handles.get(appId) === opened) forget(appId)
103
+ })
104
+ return opened
105
+ }
106
+
107
+ const kept = (appId: string): Promise<Sandbox> => {
108
+ const pending = handles.get(appId) ?? open(appId)
109
+ // Most recently used last, so the cap drops the stalest.
110
+ handles.delete(appId)
111
+ handles.set(appId, pending)
112
+ const stalest = handles.keys().next().value
113
+ if (handles.size > MAX_HANDLES && stalest !== undefined) forget(stalest)
114
+ return pending
115
+ }
116
+
117
+ /**
118
+ * Whether the sandbox is still there. Extends it only by the shortfall, so
119
+ * repeated calls never stack up time.
120
+ */
121
+ const keepAlive = async (sandbox: Sandbox): Promise<boolean> => {
122
+ const shortfall =
123
+ Date.now() + KEEP_ALIVE_MS - (sandbox.expiresAt?.getTime() ?? 0)
124
+ if (shortfall <= 0) return true
125
+ try {
126
+ await sandbox.extendTimeout(shortfall)
127
+ } catch (error) {
128
+ // Throws past the plan's session limit; the sandbox still works until then.
129
+ return !notFound(error)
130
+ }
131
+ return true
132
+ }
86
133
 
87
134
  /** The app's sandbox, created or resumed, and kept up while it's used. */
88
135
  export async function appSandbox({
@@ -90,16 +137,13 @@ export async function appSandbox({
90
137
  }: {
91
138
  appId: string
92
139
  }): Promise<Sandbox> {
93
- const pending =
94
- opening.get(appId) ?? openApp(appId).finally(() => opening.delete(appId))
95
- opening.set(appId, pending)
140
+ const pending = kept(appId)
96
141
  const sandbox = await pending
97
- // Extends only by the shortfall, so repeated calls never stack up time.
98
- const shortfall =
99
- Date.now() + KEEP_ALIVE_MS - (sandbox.expiresAt?.getTime() ?? 0)
100
- // Throws past the plan's session limit; the sandbox still works until then.
101
- if (shortfall > 0) await sandbox.extendTimeout(shortfall).catch(() => {})
102
- return sandbox
142
+ if (await keepAlive(sandbox)) return sandbox
143
+ if (handles.get(appId) === pending) forget(appId)
144
+ const reopened = await kept(appId)
145
+ await keepAlive(reopened)
146
+ return reopened
103
147
  }
104
148
 
105
149
  // A server that's booting refuses connections for a moment, and a page
@@ -118,12 +162,12 @@ export async function ensureApp({
118
162
  appId: string
119
163
  }): Promise<{ up: boolean; createdAt: Date }> {
120
164
  const sandbox = await appSandbox({ appId })
121
- const probe = await sandbox.runCommand({
165
+ const answer = await sandbox.runCommand({
122
166
  cmd: 'bash',
123
167
  args: ['-c', `${START_DEV}; ${PROBE}`],
124
168
  })
125
169
  return {
126
- up: Number(await probe.stdout()) > 0,
170
+ up: Number(await answer.stdout()) > 0,
127
171
  createdAt: sandbox.createdAt,
128
172
  }
129
173
  }
@@ -145,6 +189,7 @@ export async function deleteAppSandbox({
145
189
  }: {
146
190
  appId: string
147
191
  }): Promise<void> {
192
+ forget(appId)
148
193
  try {
149
194
  const sandbox = await Sandbox.get({
150
195
  name: sandboxName(appId),
@@ -7,9 +7,9 @@
7
7
  * your schema.
8
8
  *
9
9
  * Whatever creates them, A2 relies on the table and column names, the column
10
- * types, the primary keys, and the unique index on a2_events.event_id. The
11
- * other indexes keep its queries fast. Every name is spelled out, so Drizzle's
12
- * `casing` setting changes none of them.
10
+ * types, the primary keys, and the unique index on a2_events (session_id,
11
+ * event_id). The other indexes keep its queries fast. Every name is spelled
12
+ * out, so Drizzle's `casing` setting changes none of them.
13
13
  *
14
14
  * A2's tables change only when a release's changelog says so; compare your
15
15
  * copy with that release's file. Drizzle can't declare functions or
@@ -48,7 +48,10 @@ export const a2Events = pgTable(
48
48
  name: 'a2_events_pkey',
49
49
  columns: [table.sessionId, table.idx],
50
50
  }),
51
- uniqueIndex('a2_events_event_id').on(table.eventId),
51
+ uniqueIndex('a2_events_session_event_id').on(
52
+ table.sessionId,
53
+ table.eventId,
54
+ ),
52
55
  index('a2_events_types').on(table.sessionId, table.eventType, table.idx),
53
56
  ],
54
57
  )
@@ -0,0 +1,2 @@
1
+ CREATE UNIQUE INDEX "a2_events_session_event_id" ON "a2_events" USING btree ("session_id","event_id");--> statement-breakpoint
2
+ DROP INDEX "a2_events_event_id";