experimental-a2 0.11.0 → 0.12.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.
@@ -65,6 +65,64 @@ client exposes its `A2ClientError` subclass. See
65
65
 
66
66
  ## `experimental-a2/server`
67
67
 
68
+ ### `handler(entry)`
69
+
70
+ Define handlers inline in `createServer({ handlers })` for ordinary use.
71
+ Use this helper when composing an existing handler.
72
+
73
+ Accepts a handler function or an object with `handler` and optional `lane` and
74
+ `abortOn`. Returns the object shape: a function becomes `{ handler: entry }`,
75
+ and an object is returned unchanged. The original function and object types
76
+ are preserved. Use it to compose an existing handler without branching on its
77
+ shape.
78
+
79
+ ```ts server/composed-orders.ts
80
+ import * as a2 from 'experimental-a2'
81
+ import { createServer, handler, type HandlerContext } from 'experimental-a2/server'
82
+ import { z } from 'zod'
83
+
84
+ const orders = a2.contract({
85
+ name: 'composed-orders',
86
+ events: {
87
+ created: z.object({ shopId: z.string() }),
88
+ notified: z.object({ shopId: z.string() }),
89
+ },
90
+ })
91
+
92
+ const created = handler({
93
+ lane: 'orders',
94
+ handler: async (ctx: HandlerContext<typeof orders.events, 'created'>) => {
95
+ return orders.batch({ type: 'notified', payload: ctx.event.payload })
96
+ },
97
+ })
98
+
99
+ export const ordersServer = createServer({
100
+ contract: orders,
101
+ handlers: {
102
+ created: {
103
+ ...created,
104
+ handler: async (ctx) => {
105
+ const result = await created.handler(ctx)
106
+ // Your additional work goes here:
107
+ // await notifyShop(ctx.event.payload, { idempotencyKey: ctx.event.id })
108
+ return result
109
+ },
110
+ },
111
+ },
112
+ })
113
+ ```
114
+
115
+ Spreading the object preserves its `lane` and `abortOn`. Set either property
116
+ after the spread to override it. Changing a built-in AI handler's lane can
117
+ separate it from the handlers it coordinates with.
118
+
119
+ Composition runs in one handler attempt. Work after the original function
120
+ resolves still runs before its returned events commit. A thrown error fails
121
+ the composed attempt; on retry, the wrapper and original handler can run again.
122
+ For a new handler, annotate its context as above or pass an already typed
123
+ function. `handler` does not bind a handler to a contract;
124
+ `createServer({ handlers })` checks that it matches the contract.
125
+
68
126
  ### `createServer(options)`
69
127
 
70
128
  ```ts
@@ -1205,7 +1263,12 @@ it into `createServer({ handlers })` beside application handlers when you need
1205
1263
  a custom assembly. The table handles input facts, generation requests, model
1206
1264
  step completion, tool calls, approval responses, and terminal tool results.
1207
1265
  Application handlers spread later can deliberately replace a built-in
1208
- handler. A custom assembly owns its browser ingress policy. Use
1266
+ handler. Use `handler` from `experimental-a2/server` to normalize an
1267
+ existing entry before wrapping its `handler` and spreading its execution
1268
+ policy. Entries in this table's type are optional; narrow an entry before
1269
+ passing it to `handler`.
1270
+
1271
+ A custom assembly owns its browser ingress policy. Use
1209
1272
  `server.fetch(request, { authorize })` to reject server-authored AI event names
1210
1273
  before they reach the server. `createAgentServer()` installs the built-in
1211
1274
  browser allowlist automatically.
@@ -22,7 +22,7 @@
22
22
  "@vercel/sandbox": "^3.0.0",
23
23
  "ai": "^7.0.58",
24
24
  "codemirror": "^6.0.2",
25
- "experimental-a2": "0.11.0",
25
+ "experimental-a2": "0.12.0",
26
26
  "ioredis": "^5.9.0",
27
27
  "next": "^16.3.0",
28
28
  "pg": "^8.16.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "experimental-a2",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Durable sync and reactions for things with a lifecycle: one event log, derived state, and live client per session.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/server.ts CHANGED
@@ -301,6 +301,17 @@ export type HandlerEntry<
301
301
  handler: Handler<D, K, P>
302
302
  }
303
303
 
304
+ type HandlerFunction = (ctx: never) => Promise<unknown>
305
+
306
+ export function handler<
307
+ H extends HandlerFunction | { handler: HandlerFunction },
308
+ >(entry: H): H extends HandlerFunction ? { handler: H } : H
309
+ export function handler(
310
+ entry: HandlerFunction | { handler: HandlerFunction },
311
+ ): { handler: HandlerFunction } {
312
+ return typeof entry === 'function' ? { handler: entry } : entry
313
+ }
314
+
304
315
  export type ServerOptions<
305
316
  D extends EventDefs,
306
317
  P extends PresenceDefs = Record<never, never>,
@@ -615,8 +626,8 @@ export function createServer<
615
626
  `contract '${name}' has no event type '${type}' — cannot register a handler for it`,
616
627
  )
617
628
  }
618
- const handler = typeof entry === 'function' ? entry : entry?.handler
619
- if (typeof handler !== 'function') {
629
+ const callback = typeof entry === 'function' ? entry : entry?.handler
630
+ if (typeof callback !== 'function') {
620
631
  throw new TypeError(`handler for '${type}' must be a function`)
621
632
  }
622
633
  let abortOn: AbortMatcher | null = null
@@ -660,7 +671,7 @@ export function createServer<
660
671
  )
661
672
  }
662
673
  handlers.set(type, {
663
- handler: handler as Handler<D>,
674
+ handler: callback as Handler<D>,
664
675
  abortOn,
665
676
  lane: lane as Lane<D> | null,
666
677
  })