Skip to content

Commit 8373270

Browse files
committed
feat: add persistent checkpointer resource to agent plugin documentation
1 parent d22659b commit 8373270

1 file changed

Lines changed: 214 additions & 0 deletions

File tree

  • adminforth/documentation/docs/tutorial/08-Plugins

adminforth/documentation/docs/tutorial/08-Plugins/26-agent.md

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,8 @@ plugins: [
267267
// optional
268268
// debugField: 'debug',
269269
},
270+
// optional, see the "Persistent checkpointer" section below
271+
// checkpointResource: { ... },
270272
}),
271273
]
272274
```
@@ -275,6 +277,218 @@ Each item in `modes` defines a user-selectable preset in the chat UI. The select
275277

276278
The plugin adds a chat surface to the admin UI, keeps session history per admin user, and shows a mode picker when `modes` are configured.
277279

280+
## Persistent checkpointer
281+
282+
If you do not configure `checkpointResource`, the plugin falls back to an in-memory `MemorySaver`. This is fine for local testing, but checkpoints are lost on process restart.
283+
284+
If you want persistent LangGraph checkpoints between requests, add a dedicated resource and pass it via `checkpointResource`.
285+
286+
You can use your own table and field names. The plugin does not require a specific schema name, only a mapping for these logical fields:
287+
288+
- `idField`
289+
- `threadIdField`
290+
- `checkpointNamespaceField`
291+
- `checkpointIdField`
292+
- `parentCheckpointIdField`
293+
- `rowKindField`
294+
- `taskIdField`
295+
- `sequenceField`
296+
- `createdAtField`
297+
- `checkpointPayloadField`
298+
- `metadataPayloadField`
299+
- `writesPayloadField`
300+
- `schemaVersionField`
301+
302+
Create a resource for checkpoint rows:
303+
304+
```ts title="./resources/agent_resources/checkpoints.ts"
305+
import { AdminForthDataTypes } from 'adminforth';
306+
import type { AdminForthResourceInput } from 'adminforth';
307+
308+
export default {
309+
dataSource: 'sqlite',
310+
table: 'checkpoints',
311+
resourceId: 'checkpoints',
312+
label: 'Checkpoints',
313+
recordLabel: (record) => record.id,
314+
options: {
315+
allowedActions: {
316+
create: false,
317+
edit: false,
318+
},
319+
},
320+
columns: [
321+
{
322+
name: 'id',
323+
primaryKey: true,
324+
type: AdminForthDataTypes.STRING,
325+
showIn: {
326+
edit: false,
327+
create: false,
328+
},
329+
},
330+
{
331+
name: 'thread_id',
332+
type: AdminForthDataTypes.STRING,
333+
},
334+
{
335+
name: 'checkpoint_ns',
336+
type: AdminForthDataTypes.STRING,
337+
},
338+
{
339+
name: 'checkpoint_id',
340+
type: AdminForthDataTypes.STRING,
341+
},
342+
{
343+
name: 'parent_checkpoint_id',
344+
type: AdminForthDataTypes.STRING,
345+
},
346+
{
347+
name: 'row_kind',
348+
type: AdminForthDataTypes.STRING,
349+
enum: [
350+
{ value: 'checkpoint', label: 'Checkpoint' },
351+
{ value: 'writes', label: 'Writes' },
352+
],
353+
},
354+
{
355+
name: 'task_id',
356+
type: AdminForthDataTypes.STRING,
357+
},
358+
{
359+
name: 'seq',
360+
type: AdminForthDataTypes.INTEGER,
361+
},
362+
{
363+
name: 'created_at',
364+
type: AdminForthDataTypes.DATETIME,
365+
showIn: {
366+
edit: false,
367+
create: false,
368+
},
369+
},
370+
{
371+
name: 'checkpoint_payload',
372+
type: AdminForthDataTypes.JSON,
373+
showIn: {
374+
list: false,
375+
},
376+
},
377+
{
378+
name: 'metadata_payload',
379+
type: AdminForthDataTypes.JSON,
380+
showIn: {
381+
list: false,
382+
},
383+
},
384+
{
385+
name: 'writes_payload',
386+
type: AdminForthDataTypes.JSON,
387+
showIn: {
388+
list: false,
389+
},
390+
},
391+
{
392+
name: 'schema_version',
393+
type: AdminForthDataTypes.INTEGER,
394+
},
395+
],
396+
} as AdminForthResourceInput;
397+
```
398+
399+
Add a matching table to your schema:
400+
401+
```prisma title='./schema.prisma'
402+
model checkpoints {
403+
id String @id
404+
thread_id String
405+
checkpoint_ns String
406+
checkpoint_id String
407+
parent_checkpoint_id String?
408+
row_kind String
409+
task_id String?
410+
seq Int
411+
created_at DateTime
412+
checkpoint_payload String?
413+
metadata_payload String?
414+
writes_payload String?
415+
schema_version Int
416+
417+
@@index([thread_id, checkpoint_ns, checkpoint_id])
418+
}
419+
```
420+
421+
The payload fields can be stored as strings. The plugin serializes and deserializes checkpoint JSON on its own. The composite index on `(thread_id, checkpoint_ns, checkpoint_id)` is recommended because the checkpointer filters rows by these columns.
422+
423+
Run migration:
424+
425+
```bash
426+
pnpm makemigration --name add-adminforth-agent-checkpoints ; pnpm migrate:local
427+
```
428+
429+
Register the resource in your app:
430+
431+
```ts title="./index.ts"
432+
import checkpoints_resource from './resources/agent_resources/checkpoints.js';
433+
import sessions_resource from './resources/agent_resources/sessions.js';
434+
import turns_resource from './resources/agent_resources/turns.js';
435+
436+
export const admin = new AdminForth({
437+
...
438+
resources: [
439+
...
440+
sessions_resource,
441+
turns_resource,
442+
checkpoints_resource,
443+
],
444+
...
445+
});
446+
```
447+
448+
Then connect it to the plugin:
449+
450+
```ts title="./resources/adminuser.ts"
451+
new AdminForthAgent({
452+
modes: [
453+
...
454+
],
455+
sessionResource: {
456+
resourceId: 'sessions',
457+
idField: 'id',
458+
titleField: 'title',
459+
turnsField: 'turns',
460+
askerIdField: 'asker_id',
461+
createdAtField: 'created_at',
462+
},
463+
turnResource: {
464+
resourceId: 'turns',
465+
idField: 'id',
466+
sessionIdField: 'session_id',
467+
createdAtField: 'created_at',
468+
promptField: 'prompt',
469+
responseField: 'response',
470+
},
471+
checkpointResource: {
472+
resourceId: 'checkpoints',
473+
idField: 'id',
474+
threadIdField: 'thread_id',
475+
checkpointNamespaceField: 'checkpoint_ns',
476+
checkpointIdField: 'checkpoint_id',
477+
parentCheckpointIdField: 'parent_checkpoint_id',
478+
rowKindField: 'row_kind',
479+
taskIdField: 'task_id',
480+
sequenceField: 'seq',
481+
createdAtField: 'created_at',
482+
checkpointPayloadField: 'checkpoint_payload',
483+
metadataPayloadField: 'metadata_payload',
484+
writesPayloadField: 'writes_payload',
485+
schemaVersionField: 'schema_version',
486+
},
487+
});
488+
```
489+
490+
If your existing checkpoint table already uses different column names, keep your schema and only change the field mapping in `checkpointResource`.
491+
278492
## Reverse proxy and CDN configuration for streaming
279493

280494
The agent streams responses from `<baseURL>/adminapi/v1/agent/response` using server-sent events, where `<baseURL>` is your AdminForth base path or an empty string when deployed at the domain root. If your proxy buffers responses, the UI will receive the answer only after generation is finished.

0 commit comments

Comments
 (0)