experimental-a2 0.11.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.
- package/CHANGELOG.md +19 -0
- package/dist/{actor-D_54lz_1.d.ts → actor-DJi3RsNu.d.ts} +2 -2
- package/dist/{actor-D_54lz_1.d.ts.map → actor-DJi3RsNu.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +1 -1
- package/dist/ai-server.d.ts +12 -4
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +477 -143
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +33 -2
- package/dist/ai.d.ts.map +1 -1
- package/dist/ai.js +39 -7
- package/dist/ai.js.map +1 -1
- package/dist/scheduler-qstash.d.ts +1 -1
- package/dist/scheduler-qstash.js +1 -1
- package/dist/scheduler-vercel.d.ts +1 -1
- package/dist/scheduler-vercel.js +1 -1
- package/dist/{server-Dkz2a84E.js → server-BeNADlCI.js} +8 -5
- package/dist/server-BeNADlCI.js.map +1 -0
- package/dist/{server-DwPrMqHB.d.ts → server-DjZZa1wr.d.ts} +8 -2
- package/dist/{server-DwPrMqHB.d.ts.map → server-DjZZa1wr.d.ts.map} +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +2 -2
- package/docs/guides/06-ai-agents.mdx +100 -18
- package/docs/reference/01-api.mdx +105 -5
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +31 -16
- package/examples/playground/app/agent/[agentId]/compaction/route.ts +14 -0
- package/examples/playground/app/agent/[agentId]/compaction-event.tsx +38 -0
- package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +294 -0
- package/examples/playground/app/agent/compaction-settings.test.ts +128 -0
- package/examples/playground/app/agent/compaction-settings.ts +49 -0
- package/examples/playground/app/agent/compaction-timeline.test.ts +337 -0
- package/examples/playground/app/agent/compaction-timeline.ts +198 -0
- package/examples/playground/app/agent/model.ts +47 -1
- package/examples/playground/app/agent/server.ts +3 -2
- package/examples/playground/app/globals.css +154 -0
- package/examples/playground/package.json +1 -1
- package/package.json +1 -1
- package/src/ai-model-metadata.ts +108 -0
- package/src/ai-sdk-step.ts +5 -1
- package/src/ai-server.ts +712 -203
- package/src/ai.ts +99 -4
- package/src/server.ts +14 -3
- package/dist/server-Dkz2a84E.js.map +0 -1
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { FormEvent, ReactNode } from 'react'
|
|
4
|
+
import { useEffect, useId, useRef, useState } from 'react'
|
|
5
|
+
import type { AIState } from 'experimental-a2/ai'
|
|
6
|
+
import { formatActivityTime } from '@/app/components/activity-feed'
|
|
7
|
+
import {
|
|
8
|
+
agentModel,
|
|
9
|
+
compactionSettingsSchema,
|
|
10
|
+
latestCompactionSettings,
|
|
11
|
+
type AgentEvent,
|
|
12
|
+
type CompactionSettings,
|
|
13
|
+
} from '../model'
|
|
14
|
+
|
|
15
|
+
const number = new Intl.NumberFormat('en-US')
|
|
16
|
+
const tokens = (value: number | undefined): string =>
|
|
17
|
+
value === undefined
|
|
18
|
+
? 'Not available'
|
|
19
|
+
: `${number.format(value)} ${value === 1 ? 'token' : 'tokens'}`
|
|
20
|
+
|
|
21
|
+
function SettingsForm({
|
|
22
|
+
agentId,
|
|
23
|
+
settings,
|
|
24
|
+
closed,
|
|
25
|
+
}: {
|
|
26
|
+
agentId: string
|
|
27
|
+
settings: CompactionSettings
|
|
28
|
+
closed: boolean
|
|
29
|
+
}): ReactNode {
|
|
30
|
+
const modeId = useId()
|
|
31
|
+
const thresholdId = useId()
|
|
32
|
+
const [mode, setMode] = useState(settings.mode)
|
|
33
|
+
const [threshold, setThreshold] = useState(
|
|
34
|
+
settings.mode === 'manual' ? String(settings.thresholdTokens) : '',
|
|
35
|
+
)
|
|
36
|
+
const [saving, setSaving] = useState(false)
|
|
37
|
+
const [error, setError] = useState<string | null>(null)
|
|
38
|
+
const [notice, setNotice] = useState<string | null>(null)
|
|
39
|
+
const pending = useRef<AbortController | null>(null)
|
|
40
|
+
|
|
41
|
+
useEffect(() => () => pending.current?.abort(), [])
|
|
42
|
+
|
|
43
|
+
const save = async (next: CompactionSettings): Promise<void> => {
|
|
44
|
+
pending.current?.abort()
|
|
45
|
+
const controller = new AbortController()
|
|
46
|
+
pending.current = controller
|
|
47
|
+
setSaving(true)
|
|
48
|
+
setError(null)
|
|
49
|
+
setNotice(null)
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetch(
|
|
52
|
+
`/agent/${encodeURIComponent(agentId)}/compaction`,
|
|
53
|
+
{
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'content-type': 'application/json' },
|
|
56
|
+
body: JSON.stringify({ id: crypto.randomUUID(), settings: next }),
|
|
57
|
+
signal: controller.signal,
|
|
58
|
+
},
|
|
59
|
+
)
|
|
60
|
+
if (!response.ok)
|
|
61
|
+
throw new Error('Could not save compaction settings. Try again.')
|
|
62
|
+
setNotice('Saved. The live feed will confirm the latest settings.')
|
|
63
|
+
} catch (cause) {
|
|
64
|
+
if (!controller.signal.aborted)
|
|
65
|
+
setError(cause instanceof Error ? cause.message : String(cause))
|
|
66
|
+
} finally {
|
|
67
|
+
if (pending.current === controller) {
|
|
68
|
+
pending.current = null
|
|
69
|
+
setSaving(false)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const submit = (event: FormEvent): void => {
|
|
75
|
+
event.preventDefault()
|
|
76
|
+
const parsed = compactionSettingsSchema.safeParse(
|
|
77
|
+
mode === 'manual'
|
|
78
|
+
? { mode, thresholdTokens: Number(threshold) }
|
|
79
|
+
: { mode },
|
|
80
|
+
)
|
|
81
|
+
if (!parsed.success) {
|
|
82
|
+
setError('Enter a positive whole-number threshold.')
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
void save(parsed.data)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<form className="compaction-form" onSubmit={submit}>
|
|
90
|
+
<fieldset disabled={saving || closed}>
|
|
91
|
+
<div className="compaction-fields">
|
|
92
|
+
<label htmlFor={modeId}>
|
|
93
|
+
Mode
|
|
94
|
+
<select
|
|
95
|
+
id={modeId}
|
|
96
|
+
value={mode}
|
|
97
|
+
onChange={(event) =>
|
|
98
|
+
setMode(event.target.value as CompactionSettings['mode'])
|
|
99
|
+
}
|
|
100
|
+
>
|
|
101
|
+
<option value="automatic">Automatic</option>
|
|
102
|
+
<option value="manual">Custom threshold</option>
|
|
103
|
+
<option value="disabled">Disable further compaction</option>
|
|
104
|
+
</select>
|
|
105
|
+
</label>
|
|
106
|
+
<label htmlFor={thresholdId}>
|
|
107
|
+
Threshold in tokens
|
|
108
|
+
<input
|
|
109
|
+
id={thresholdId}
|
|
110
|
+
type="number"
|
|
111
|
+
min={1}
|
|
112
|
+
max={Number.MAX_SAFE_INTEGER}
|
|
113
|
+
step={1}
|
|
114
|
+
required={mode === 'manual'}
|
|
115
|
+
disabled={mode !== 'manual'}
|
|
116
|
+
value={threshold}
|
|
117
|
+
onChange={(event) => setThreshold(event.target.value)}
|
|
118
|
+
placeholder="Automatic"
|
|
119
|
+
/>
|
|
120
|
+
</label>
|
|
121
|
+
</div>
|
|
122
|
+
<div className="compaction-actions">
|
|
123
|
+
<button type="submit">{saving ? 'Saving…' : 'Apply settings'}</button>
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
className="secondary"
|
|
127
|
+
onClick={() => void save({ mode: 'manual', thresholdTokens: 1 })}
|
|
128
|
+
>
|
|
129
|
+
Set to 1 token
|
|
130
|
+
</button>
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
className="secondary"
|
|
134
|
+
onClick={() => void save({ mode: 'automatic' })}
|
|
135
|
+
>
|
|
136
|
+
Reset to automatic
|
|
137
|
+
</button>
|
|
138
|
+
</div>
|
|
139
|
+
</fieldset>
|
|
140
|
+
<p className="hint">
|
|
141
|
+
Changes apply to the next step that has not started and do not start a
|
|
142
|
+
new turn. Send a message to test. At 1 token, every eligible step
|
|
143
|
+
compacts until you reset it.
|
|
144
|
+
</p>
|
|
145
|
+
{mode === 'disabled' ? (
|
|
146
|
+
<p className="hint">
|
|
147
|
+
Existing summaries remain in use. This stops future compaction.
|
|
148
|
+
</p>
|
|
149
|
+
) : null}
|
|
150
|
+
{notice ? (
|
|
151
|
+
<p role="status" className="hint">
|
|
152
|
+
{notice}
|
|
153
|
+
</p>
|
|
154
|
+
) : null}
|
|
155
|
+
{error ? (
|
|
156
|
+
<p role="alert" className="error">
|
|
157
|
+
{error}
|
|
158
|
+
</p>
|
|
159
|
+
) : null}
|
|
160
|
+
</form>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function CompactionPanel({
|
|
165
|
+
agentId,
|
|
166
|
+
state,
|
|
167
|
+
events,
|
|
168
|
+
}: {
|
|
169
|
+
agentId: string
|
|
170
|
+
state: AIState
|
|
171
|
+
events: readonly AgentEvent[]
|
|
172
|
+
}): ReactNode {
|
|
173
|
+
const { settings, id } = latestCompactionSettings(events)
|
|
174
|
+
const metadata = state.modelMetadata[agentModel]
|
|
175
|
+
const limits = metadata?.status === 'resolved' ? metadata.limits : undefined
|
|
176
|
+
const automaticThreshold =
|
|
177
|
+
limits === undefined ? undefined : Math.floor(limits.contextWindow * 0.75)
|
|
178
|
+
const effectiveThreshold =
|
|
179
|
+
settings.mode === 'manual' ? settings.thresholdTokens : automaticThreshold
|
|
180
|
+
const lastStep = events.findLast(
|
|
181
|
+
(event) => event.type === 'ai.generation.completed',
|
|
182
|
+
)
|
|
183
|
+
const completed = events
|
|
184
|
+
.filter((event) => event.type === 'ai.compaction.completed')
|
|
185
|
+
.toReversed()
|
|
186
|
+
const compacting = state.compaction?.status === 'running'
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<details className="agent-compaction">
|
|
190
|
+
<summary>
|
|
191
|
+
<span className="compaction-chevron" aria-hidden="true">
|
|
192
|
+
›
|
|
193
|
+
</span>
|
|
194
|
+
<strong>Compaction</strong>
|
|
195
|
+
<span className={`badge${compacting ? ' live' : ''}`}>
|
|
196
|
+
{compacting
|
|
197
|
+
? 'Compacting…'
|
|
198
|
+
: settings.mode === 'disabled'
|
|
199
|
+
? 'Disabled'
|
|
200
|
+
: settings.mode === 'manual'
|
|
201
|
+
? `Custom · ${tokens(settings.thresholdTokens)}`
|
|
202
|
+
: 'Automatic'}
|
|
203
|
+
</span>
|
|
204
|
+
</summary>
|
|
205
|
+
<div className="compaction-controls">
|
|
206
|
+
<p className="hint">
|
|
207
|
+
Model: <code>{agentModel}</code>. Limits are saved for this session.
|
|
208
|
+
</p>
|
|
209
|
+
<dl className="compaction-metrics">
|
|
210
|
+
<div>
|
|
211
|
+
<dt>Context window</dt>
|
|
212
|
+
<dd>{tokens(limits?.contextWindow)}</dd>
|
|
213
|
+
</div>
|
|
214
|
+
<div>
|
|
215
|
+
<dt>Model output ceiling</dt>
|
|
216
|
+
<dd>{tokens(limits?.maxOutputTokens)}</dd>
|
|
217
|
+
</div>
|
|
218
|
+
<div>
|
|
219
|
+
<dt>Automatic trigger · 75%</dt>
|
|
220
|
+
<dd>{tokens(automaticThreshold)}</dd>
|
|
221
|
+
</div>
|
|
222
|
+
<div>
|
|
223
|
+
<dt>Threshold for next step</dt>
|
|
224
|
+
<dd>
|
|
225
|
+
{settings.mode === 'disabled'
|
|
226
|
+
? 'Disabled'
|
|
227
|
+
: tokens(effectiveThreshold)}
|
|
228
|
+
</dd>
|
|
229
|
+
</div>
|
|
230
|
+
<div>
|
|
231
|
+
<dt>Last step input · reported</dt>
|
|
232
|
+
<dd>{tokens(lastStep?.payload.usage?.inputTokens)}</dd>
|
|
233
|
+
</div>
|
|
234
|
+
<div>
|
|
235
|
+
<dt>Last step input · estimated</dt>
|
|
236
|
+
<dd>{tokens(lastStep?.payload.inputTokenEstimate)}</dd>
|
|
237
|
+
</div>
|
|
238
|
+
</dl>
|
|
239
|
+
{limits === undefined ? (
|
|
240
|
+
<p className="hint">
|
|
241
|
+
{metadata?.status === 'unavailable'
|
|
242
|
+
? 'Gateway did not report limits for this model. You can set a custom threshold.'
|
|
243
|
+
: metadata?.status === 'pending'
|
|
244
|
+
? 'Model limits have been requested but are not resolved yet. The agent can continue.'
|
|
245
|
+
: 'Send a message to discover the model limits, or set a custom threshold first.'}
|
|
246
|
+
</p>
|
|
247
|
+
) : null}
|
|
248
|
+
<SettingsForm
|
|
249
|
+
key={id}
|
|
250
|
+
agentId={agentId}
|
|
251
|
+
settings={settings}
|
|
252
|
+
closed={state.status === 'closed'}
|
|
253
|
+
/>
|
|
254
|
+
<details className="compaction-history">
|
|
255
|
+
<summary>
|
|
256
|
+
Completed compactions <span>{completed.length}</span>
|
|
257
|
+
</summary>
|
|
258
|
+
{completed.length === 0 ? (
|
|
259
|
+
<p className="hint">
|
|
260
|
+
No completed compactions yet. Each completion will stay here after
|
|
261
|
+
reload.
|
|
262
|
+
</p>
|
|
263
|
+
) : (
|
|
264
|
+
<ol>
|
|
265
|
+
{completed.map((event) => (
|
|
266
|
+
<li key={event.id}>
|
|
267
|
+
<div className="compaction-marker">
|
|
268
|
+
<strong>Context compacted · #{event.index}</strong>
|
|
269
|
+
<time dateTime={event.createdAt.toISOString()}>
|
|
270
|
+
{formatActivityTime(event.createdAt)}
|
|
271
|
+
</time>
|
|
272
|
+
</div>
|
|
273
|
+
<p className="hint">
|
|
274
|
+
{event.payload.throughIndex === undefined
|
|
275
|
+
? `Through message ${event.payload.throughMessageId}`
|
|
276
|
+
: `Summarized through event #${event.payload.throughIndex}`}{' '}
|
|
277
|
+
· {tokens(event.payload.usage?.inputTokens)} in ·{' '}
|
|
278
|
+
{tokens(event.payload.usage?.outputTokens)} out
|
|
279
|
+
</p>
|
|
280
|
+
{event.payload.summary ? (
|
|
281
|
+
<details>
|
|
282
|
+
<summary>View summary</summary>
|
|
283
|
+
<pre>{event.payload.summary}</pre>
|
|
284
|
+
</details>
|
|
285
|
+
) : null}
|
|
286
|
+
</li>
|
|
287
|
+
))}
|
|
288
|
+
</ol>
|
|
289
|
+
)}
|
|
290
|
+
</details>
|
|
291
|
+
</div>
|
|
292
|
+
</details>
|
|
293
|
+
)
|
|
294
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { expect, it } from 'vitest'
|
|
2
|
+
import { createAgentServer } from 'experimental-a2/ai/server'
|
|
3
|
+
import { memory } from 'experimental-a2/store-memory'
|
|
4
|
+
import { compactionOptions, demoAgent, latestCompactionSettings } from './model'
|
|
5
|
+
import { updateCompactionSettings } from './compaction-settings'
|
|
6
|
+
|
|
7
|
+
const request = (
|
|
8
|
+
settings: unknown,
|
|
9
|
+
options: { origin?: string; contentType?: string; id?: string } = {},
|
|
10
|
+
): Request =>
|
|
11
|
+
new Request('https://example.test/agent/session/compaction', {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: {
|
|
14
|
+
origin: options.origin ?? 'https://example.test',
|
|
15
|
+
'content-type': options.contentType ?? 'application/json',
|
|
16
|
+
},
|
|
17
|
+
body: JSON.stringify({ id: options.id ?? crypto.randomUUID(), settings }),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('uses the request Host when Next normalizes the internal URL', async () => {
|
|
21
|
+
let writes = 0
|
|
22
|
+
const response = await updateCompactionSettings({
|
|
23
|
+
request: new Request('http://localhost:3107/agent/s/compaction', {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
host: '127.0.0.1:3107',
|
|
27
|
+
origin: 'http://127.0.0.1:3107',
|
|
28
|
+
'content-type': 'application/json',
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
id: crypto.randomUUID(),
|
|
32
|
+
settings: { mode: 'manual', thresholdTokens: 1 },
|
|
33
|
+
}),
|
|
34
|
+
}),
|
|
35
|
+
sessionId: 's',
|
|
36
|
+
append: async () => {
|
|
37
|
+
writes += 1
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
expect(response.status).toBe(200)
|
|
41
|
+
expect(writes).toBe(1)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('saves session settings durably and idempotently without starting a turn', async () => {
|
|
45
|
+
let generations = 0
|
|
46
|
+
const server = createAgentServer({
|
|
47
|
+
agent: demoAgent,
|
|
48
|
+
store: memory(),
|
|
49
|
+
model: 'test/model',
|
|
50
|
+
compaction: ({ history }) => compactionOptions(history),
|
|
51
|
+
generate: () => {
|
|
52
|
+
generations += 1
|
|
53
|
+
return new ReadableStream({
|
|
54
|
+
start(controller) {
|
|
55
|
+
controller.close()
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
const session = server.session('settings-test')
|
|
61
|
+
const id = crypto.randomUUID()
|
|
62
|
+
const save = () =>
|
|
63
|
+
updateCompactionSettings({
|
|
64
|
+
request: request({ mode: 'manual', thresholdTokens: 1 }, { id }),
|
|
65
|
+
sessionId: session.id,
|
|
66
|
+
append: (event) => session.append(event),
|
|
67
|
+
})
|
|
68
|
+
expect((await save()).status).toBe(200)
|
|
69
|
+
expect((await save()).status).toBe(200)
|
|
70
|
+
await server.drain(session.id)
|
|
71
|
+
const history = await session.history()
|
|
72
|
+
expect(history.map((event) => event.type)).toEqual([
|
|
73
|
+
'compaction.settings.updated',
|
|
74
|
+
])
|
|
75
|
+
expect(latestCompactionSettings(history).settings).toEqual({
|
|
76
|
+
mode: 'manual',
|
|
77
|
+
thresholdTokens: 1,
|
|
78
|
+
})
|
|
79
|
+
expect(compactionOptions(history)).toEqual({ thresholdTokens: 1 })
|
|
80
|
+
expect(compactionOptions(await server.session('other').history())).toEqual({})
|
|
81
|
+
expect(generations).toBe(0)
|
|
82
|
+
|
|
83
|
+
await updateCompactionSettings({
|
|
84
|
+
request: request({ mode: 'disabled' }),
|
|
85
|
+
sessionId: session.id,
|
|
86
|
+
append: (event) => session.append(event),
|
|
87
|
+
})
|
|
88
|
+
expect(compactionOptions(await session.history())).toBe(false)
|
|
89
|
+
await updateCompactionSettings({
|
|
90
|
+
request: request({ mode: 'automatic' }),
|
|
91
|
+
sessionId: session.id,
|
|
92
|
+
append: (event) => session.append(event),
|
|
93
|
+
})
|
|
94
|
+
expect(compactionOptions(await session.history())).toEqual({})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it.each([0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1, '1', null])(
|
|
98
|
+
'rejects invalid threshold %s',
|
|
99
|
+
async (thresholdTokens) => {
|
|
100
|
+
let writes = 0
|
|
101
|
+
const response = await updateCompactionSettings({
|
|
102
|
+
request: request({ mode: 'manual', thresholdTokens }),
|
|
103
|
+
sessionId: 's',
|
|
104
|
+
append: async () => {
|
|
105
|
+
writes += 1
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
expect(response.status).toBe(400)
|
|
109
|
+
expect(writes).toBe(0)
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
it.each([
|
|
114
|
+
{ origin: 'https://other.test', status: 403 },
|
|
115
|
+
{ contentType: 'text/plain', status: 415 },
|
|
116
|
+
{ id: 'not-an-id', status: 400 },
|
|
117
|
+
])('rejects invalid request boundaries %j', async ({ status, ...options }) => {
|
|
118
|
+
let writes = 0
|
|
119
|
+
const response = await updateCompactionSettings({
|
|
120
|
+
request: request({ mode: 'automatic' }, options),
|
|
121
|
+
sessionId: 's',
|
|
122
|
+
append: async () => {
|
|
123
|
+
writes += 1
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
expect(response.status).toBe(status)
|
|
127
|
+
expect(writes).toBe(0)
|
|
128
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { compactionSettingsSchema, type CompactionSettings } from './model'
|
|
3
|
+
|
|
4
|
+
const updateSchema = z.strictObject({
|
|
5
|
+
id: z.uuid(),
|
|
6
|
+
settings: compactionSettingsSchema,
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
export async function updateCompactionSettings(options: {
|
|
10
|
+
request: Request
|
|
11
|
+
sessionId: string
|
|
12
|
+
append: (event: {
|
|
13
|
+
id: string
|
|
14
|
+
type: 'compaction.settings.updated'
|
|
15
|
+
payload: CompactionSettings
|
|
16
|
+
}) => Promise<unknown>
|
|
17
|
+
}): Promise<Response> {
|
|
18
|
+
const { request, sessionId } = options
|
|
19
|
+
const origin = request.headers.get('origin')
|
|
20
|
+
const requestUrl = new URL(request.url)
|
|
21
|
+
requestUrl.host = request.headers.get('host') ?? requestUrl.host
|
|
22
|
+
if (origin !== null && origin !== requestUrl.origin) {
|
|
23
|
+
return Response.json(
|
|
24
|
+
{ error: 'Use this page to update compaction settings.' },
|
|
25
|
+
{ status: 403 },
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
if (
|
|
29
|
+
request.headers.get('content-type')?.split(';')[0]?.trim() !==
|
|
30
|
+
'application/json'
|
|
31
|
+
) {
|
|
32
|
+
return Response.json({ error: 'Expected JSON settings.' }, { status: 415 })
|
|
33
|
+
}
|
|
34
|
+
if (sessionId.length === 0)
|
|
35
|
+
return Response.json({ error: 'Session is required.' }, { status: 400 })
|
|
36
|
+
const parsed = updateSchema.safeParse(await request.json().catch(() => null))
|
|
37
|
+
if (!parsed.success) {
|
|
38
|
+
return Response.json(
|
|
39
|
+
{ error: 'Choose a mode and a positive whole-number threshold.' },
|
|
40
|
+
{ status: 400 },
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
await options.append({
|
|
44
|
+
id: `agent.compaction.settings:${encodeURIComponent(sessionId)}:${parsed.data.id}`,
|
|
45
|
+
type: 'compaction.settings.updated',
|
|
46
|
+
payload: parsed.data.settings,
|
|
47
|
+
})
|
|
48
|
+
return Response.json({ settings: parsed.data.settings })
|
|
49
|
+
}
|