experimental-a2 0.12.0 → 0.13.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.
@@ -1,3 +1,49 @@
1
1
  import { agent } from 'experimental-a2/ai'
2
+ import type { ContractEvent } from 'experimental-a2'
3
+ import type { AutomaticCompaction } from 'experimental-a2/ai/server'
4
+ import { z } from 'zod'
2
5
 
3
- export const demoAgent = agent({ name: 'agent' })
6
+ export const agentModel = 'openai/gpt-5.6-luna'
7
+
8
+ export const compactionSettingsSchema = z.discriminatedUnion('mode', [
9
+ z.strictObject({ mode: z.literal('automatic') }),
10
+ z.strictObject({
11
+ mode: z.literal('manual'),
12
+ thresholdTokens: z.number().int().min(1).max(Number.MAX_SAFE_INTEGER),
13
+ }),
14
+ z.strictObject({ mode: z.literal('disabled') }),
15
+ ])
16
+
17
+ export type CompactionSettings = z.infer<typeof compactionSettingsSchema>
18
+
19
+ export const demoAgent = agent({
20
+ name: 'agent',
21
+ events: { 'compaction.settings.updated': compactionSettingsSchema },
22
+ })
23
+
24
+ export type AgentEvent = ContractEvent<typeof demoAgent.contract.events>
25
+
26
+ const automatic: CompactionSettings = { mode: 'automatic' }
27
+
28
+ export function latestCompactionSettings(history: readonly AgentEvent[]): {
29
+ settings: CompactionSettings
30
+ id: string
31
+ } {
32
+ const event = history.findLast(
33
+ (candidate) => candidate.type === 'compaction.settings.updated',
34
+ )
35
+ return event
36
+ ? { settings: event.payload, id: event.id }
37
+ : { settings: automatic, id: 'default' }
38
+ }
39
+
40
+ export function compactionOptions(
41
+ history: readonly AgentEvent[],
42
+ ): AutomaticCompaction | false {
43
+ const { settings } = latestCompactionSettings(history)
44
+ return settings.mode === 'disabled'
45
+ ? false
46
+ : settings.mode === 'manual'
47
+ ? { thresholdTokens: settings.thresholdTokens }
48
+ : {}
49
+ }
@@ -8,7 +8,7 @@ import { createAgentServer } from 'experimental-a2/ai/server'
8
8
  import { vercelQueues } from 'experimental-a2/scheduler-vercel'
9
9
  import { z } from 'zod'
10
10
  import { store } from '@/lib/store'
11
- import { demoAgent } from './model'
11
+ import { agentModel, compactionOptions, demoAgent } from './model'
12
12
 
13
13
  const maxReminderDelaySeconds = 6 * 24 * 60 * 60
14
14
  const bashTimeoutMs = 120_000
@@ -131,7 +131,8 @@ export const agentServer = createAgentServer({
131
131
  ...(store ? { store } : {}),
132
132
  ...(agentScheduler ? { scheduler: agentScheduler } : {}),
133
133
  agent: demoAgent,
134
- model: 'openai/gpt-5.6-luna',
134
+ model: agentModel,
135
+ compaction: ({ history }) => compactionOptions(history),
135
136
  tools,
136
137
  instructions: `You are an incident-response agent for the playground.
137
138
  When responding to an incident, inspect checkout-api before taking action. Its
@@ -1489,6 +1489,160 @@ button.danger {
1489
1489
 
1490
1490
  /* ── durable agent ──────────────────────────────────────────────── */
1491
1491
 
1492
+ .agent-compaction {
1493
+ margin: 1rem 0;
1494
+ padding: 0.75rem 1rem;
1495
+ border: 1px solid var(--line);
1496
+ border-radius: 12px;
1497
+ background: var(--surface);
1498
+ }
1499
+
1500
+ .compaction-marker {
1501
+ display: flex;
1502
+ align-items: center;
1503
+ justify-content: space-between;
1504
+ flex-wrap: wrap;
1505
+ gap: 0.5rem;
1506
+ }
1507
+
1508
+ .agent-compaction > summary {
1509
+ display: flex;
1510
+ align-items: center;
1511
+ gap: 0.5rem;
1512
+ cursor: pointer;
1513
+ list-style: none;
1514
+ }
1515
+
1516
+ .agent-compaction > summary::-webkit-details-marker {
1517
+ display: none;
1518
+ }
1519
+
1520
+ .compaction-chevron {
1521
+ color: var(--muted);
1522
+ }
1523
+
1524
+ .agent-compaction[open] .compaction-chevron {
1525
+ transform: rotate(90deg);
1526
+ }
1527
+
1528
+ .agent-compaction > summary > strong {
1529
+ flex: 1;
1530
+ font-size: 0.9rem;
1531
+ }
1532
+
1533
+ .compaction-controls {
1534
+ margin-top: 0.75rem;
1535
+ padding-top: 0.75rem;
1536
+ border-top: 1px solid var(--line);
1537
+ }
1538
+
1539
+ .agent-compaction code {
1540
+ overflow-wrap: anywhere;
1541
+ }
1542
+
1543
+ .compaction-metrics {
1544
+ display: grid;
1545
+ grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
1546
+ gap: 0.8rem;
1547
+ margin: 1rem 0;
1548
+ }
1549
+
1550
+ .compaction-metrics dt {
1551
+ color: var(--muted);
1552
+ font-size: 0.8rem;
1553
+ }
1554
+
1555
+ .compaction-metrics dd {
1556
+ margin: 0.2rem 0 0;
1557
+ font-weight: 600;
1558
+ font-variant-numeric: tabular-nums;
1559
+ }
1560
+
1561
+ .compaction-form {
1562
+ margin: 0;
1563
+ padding: 0;
1564
+ border: 0;
1565
+ }
1566
+
1567
+ .compaction-form fieldset {
1568
+ margin: 0;
1569
+ padding: 0;
1570
+ border: 0;
1571
+ }
1572
+
1573
+ .compaction-fields {
1574
+ display: grid;
1575
+ grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
1576
+ gap: 0.75rem;
1577
+ }
1578
+
1579
+ .compaction-actions {
1580
+ display: flex;
1581
+ flex-wrap: wrap;
1582
+ gap: 0.5rem;
1583
+ margin-top: 0.75rem;
1584
+ }
1585
+
1586
+ .compaction-history {
1587
+ margin-top: 1rem;
1588
+ padding-top: 0.75rem;
1589
+ border-top: 1px solid var(--line);
1590
+ }
1591
+
1592
+ .compaction-history summary {
1593
+ cursor: pointer;
1594
+ font-size: 0.9rem;
1595
+ }
1596
+
1597
+ .compaction-history ol {
1598
+ padding: 0;
1599
+ list-style: none;
1600
+ }
1601
+
1602
+ .compaction-history li + li {
1603
+ border-top: 1px solid var(--line);
1604
+ margin-top: 0.75rem;
1605
+ padding-top: 0.75rem;
1606
+ }
1607
+
1608
+ .compaction-marker {
1609
+ font-size: 0.85rem;
1610
+ }
1611
+
1612
+ .compaction-marker time {
1613
+ color: var(--muted);
1614
+ }
1615
+
1616
+ .compaction-history pre,
1617
+ .agent-compaction-event pre {
1618
+ white-space: pre-wrap;
1619
+ overflow-wrap: anywhere;
1620
+ max-height: 20rem;
1621
+ overflow: auto;
1622
+ }
1623
+
1624
+ .agent-compaction-event {
1625
+ padding: 0.65rem 0.85rem;
1626
+ border: 1px dashed var(--line);
1627
+ border-radius: 9px;
1628
+ background: var(--code-bg);
1629
+ }
1630
+
1631
+ .agent-compaction-event summary {
1632
+ cursor: pointer;
1633
+ font-size: 0.85rem;
1634
+ color: var(--muted);
1635
+ }
1636
+
1637
+ .agent-compaction-event summary > span {
1638
+ font-weight: 600;
1639
+ }
1640
+
1641
+ .agent-compaction-event time {
1642
+ display: inline-block;
1643
+ margin-left: 0.75rem;
1644
+ }
1645
+
1492
1646
  .agent-conversation {
1493
1647
  display: grid;
1494
1648
  overflow: hidden;
@@ -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.12.0",
25
+ "experimental-a2": "0.13.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.12.0",
3
+ "version": "0.13.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",
@@ -0,0 +1,108 @@
1
+ import type { LanguageModel } from 'ai'
2
+ import type { ModelLimits } from './ai.ts'
3
+
4
+ const catalogUrl = 'https://ai-gateway.vercel.sh/v1/models'
5
+ const catalogLifetimeMs = 60 * 60 * 1_000
6
+ const catalogTimeoutMs = 5_000
7
+
8
+ let catalog:
9
+ | {
10
+ expiresAt: number
11
+ promise: Promise<ReadonlyMap<string, ModelLimits>>
12
+ }
13
+ | undefined
14
+
15
+ export const gatewayModelId = (model: LanguageModel): string | undefined => {
16
+ if (typeof model === 'string') {
17
+ return globalThis.AI_SDK_DEFAULT_PROVIDER === undefined ? model : undefined
18
+ }
19
+ return model.provider === 'gateway' ? model.modelId : undefined
20
+ }
21
+
22
+ const readCatalog = (): Promise<ReadonlyMap<string, ModelLimits>> => {
23
+ if (catalog && catalog.expiresAt > Date.now()) return catalog.promise
24
+ const entry = {
25
+ expiresAt: Number.POSITIVE_INFINITY,
26
+ promise: (async () => {
27
+ const controller = new AbortController()
28
+ const timeout = setTimeout(
29
+ () => controller.abort(new Error('model catalog request timed out')),
30
+ catalogTimeoutMs,
31
+ )
32
+ timeout.unref?.()
33
+ try {
34
+ const response = await fetch(catalogUrl, { signal: controller.signal })
35
+ if (!response.ok)
36
+ throw new Error(
37
+ `model catalog request failed: HTTP ${response.status}`,
38
+ )
39
+ const body: unknown = await response.json()
40
+ if (
41
+ typeof body !== 'object' ||
42
+ body === null ||
43
+ !('data' in body) ||
44
+ !Array.isArray(body.data)
45
+ ) {
46
+ throw new Error('model catalog response must contain a data array')
47
+ }
48
+ const models = new Map<string, ModelLimits>()
49
+ for (const row of body.data as unknown[]) {
50
+ if (
51
+ typeof row !== 'object' ||
52
+ row === null ||
53
+ !('type' in row) ||
54
+ !('id' in row) ||
55
+ !('context_window' in row) ||
56
+ !('max_tokens' in row) ||
57
+ row.type !== 'language' ||
58
+ typeof row.id !== 'string' ||
59
+ typeof row.context_window !== 'number' ||
60
+ typeof row.max_tokens !== 'number' ||
61
+ !Number.isSafeInteger(row.context_window) ||
62
+ row.context_window <= 0 ||
63
+ !Number.isSafeInteger(row.max_tokens) ||
64
+ row.max_tokens <= 0
65
+ )
66
+ continue
67
+ models.set(row.id, {
68
+ contextWindow: row.context_window,
69
+ maxOutputTokens: row.max_tokens,
70
+ })
71
+ }
72
+ return models
73
+ } finally {
74
+ clearTimeout(timeout)
75
+ }
76
+ })(),
77
+ }
78
+ catalog = entry
79
+ entry.promise = entry.promise.then(
80
+ (models) => {
81
+ entry.expiresAt = Date.now() + catalogLifetimeMs
82
+ return models
83
+ },
84
+ (error: unknown) => {
85
+ if (catalog === entry) catalog = undefined
86
+ throw error
87
+ },
88
+ )
89
+ return entry.promise
90
+ }
91
+
92
+ export const readModelLimits = async (options: {
93
+ modelId: string
94
+ signal: AbortSignal
95
+ }): Promise<ModelLimits | null> => {
96
+ options.signal.throwIfAborted()
97
+ let onAbort!: () => void
98
+ const aborted = new Promise<never>((_resolve, reject) => {
99
+ onAbort = () => reject(options.signal.reason)
100
+ options.signal.addEventListener('abort', onAbort, { once: true })
101
+ })
102
+ try {
103
+ const models = await Promise.race([readCatalog(), aborted])
104
+ return models.get(options.modelId) ?? null
105
+ } finally {
106
+ options.signal.removeEventListener('abort', onAbort)
107
+ }
108
+ }
@@ -10,6 +10,7 @@ import type {
10
10
  Instructions,
11
11
  LanguageModel,
12
12
  LanguageModelUsage,
13
+ ModelMessage,
13
14
  TextStreamPart,
14
15
  ToolSet,
15
16
  UIMessage,
@@ -127,6 +128,7 @@ export type AISDKStepInput<M extends UIMessage, T extends ToolSet> = {
127
128
  model: LanguageModel
128
129
  tools: T
129
130
  messages: M[]
131
+ modelMessages?: ModelMessage[]
130
132
  responseMessageId: string
131
133
  abortSignal: AbortSignal
132
134
  instructions?: Instructions
@@ -201,7 +203,9 @@ export async function generateAISDKStep<M extends UIMessage, T extends ToolSet>(
201
203
  ): Promise<AISDKStepResult<M>> {
202
204
  const settings = safeSettings(input.settings)
203
205
  const tools = modelToolSet(input.tools)
204
- const messages = await convertToModelMessages(input.messages, { tools })
206
+ const messages =
207
+ input.modelMessages ??
208
+ (await convertToModelMessages(input.messages, { tools }))
205
209
  const streamOptions = {
206
210
  ...settings,
207
211
  model: input.model,