copilotkit 4.17.0 → 4.18.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 (81) hide show
  1. package/README.md +127 -5
  2. package/cli-build-info.json +7 -7
  3. package/exporters/langgraph/README.md +118 -0
  4. package/exporters/langgraph/export_checkpointer.py +125 -0
  5. package/index.js +12227 -9022
  6. package/onboarding/index.json +1 -1
  7. package/onboarding/prompts/authenticate/start.md +18 -19
  8. package/onboarding/prompts/conversion/plan.md +3 -3
  9. package/onboarding/prompts/credentials/finalize-plan.md +57 -200
  10. package/onboarding/prompts/credentials/plan.md +24 -23
  11. package/onboarding/prompts/credentials/settle-credentials.md +40 -213
  12. package/onboarding/prompts/credentials/write-plan.md +17 -8
  13. package/onboarding/prompts/fallback/best-effort.md +12 -12
  14. package/onboarding/prompts/feature/a2ui/implement.md +7 -7
  15. package/onboarding/prompts/feature/a2ui/proof.md +7 -7
  16. package/onboarding/prompts/feature/a2ui/start.md +53 -9
  17. package/onboarding/prompts/feature/channels/implement.md +8 -8
  18. package/onboarding/prompts/feature/channels/proof.md +7 -7
  19. package/onboarding/prompts/feature/channels/start.md +50 -7
  20. package/onboarding/prompts/feature/chat-suggestions/implement.md +7 -7
  21. package/onboarding/prompts/feature/chat-suggestions/proof.md +7 -7
  22. package/onboarding/prompts/feature/chat-suggestions/start.md +50 -7
  23. package/onboarding/prompts/feature/complete.md +2 -2
  24. package/onboarding/prompts/feature/learning/implement.md +24 -19
  25. package/onboarding/prompts/feature/learning/proof.md +8 -8
  26. package/onboarding/prompts/feature/learning/start.md +43 -20
  27. package/onboarding/prompts/feature/open-generative-ui/implement.md +7 -7
  28. package/onboarding/prompts/feature/open-generative-ui/proof.md +7 -7
  29. package/onboarding/prompts/feature/open-generative-ui/start.md +50 -7
  30. package/onboarding/prompts/feature/realtime-sync/implement.md +8 -8
  31. package/onboarding/prompts/feature/realtime-sync/proof.md +7 -7
  32. package/onboarding/prompts/feature/realtime-sync/start.md +49 -6
  33. package/onboarding/prompts/feature/rich-threads/implement.md +9 -9
  34. package/onboarding/prompts/feature/rich-threads/proof.md +7 -7
  35. package/onboarding/prompts/feature/rich-threads/start.md +49 -6
  36. package/onboarding/prompts/feature/stop.md +5 -5
  37. package/onboarding/prompts/feature/voice/implement.md +7 -7
  38. package/onboarding/prompts/feature/voice/proof.md +7 -7
  39. package/onboarding/prompts/feature/voice/start.md +50 -7
  40. package/onboarding/prompts/framework/ag2.md +2 -2
  41. package/onboarding/prompts/framework/agno.md +2 -2
  42. package/onboarding/prompts/framework/built-in.md +2 -2
  43. package/onboarding/prompts/framework/claude-sdk-python.md +2 -2
  44. package/onboarding/prompts/framework/claude-sdk-typescript.md +2 -2
  45. package/onboarding/prompts/framework/crewai-flows.md +2 -2
  46. package/onboarding/prompts/framework/deep-agents.md +2 -2
  47. package/onboarding/prompts/framework/google-adk.md +2 -2
  48. package/onboarding/prompts/framework/langgraph-fastapi.md +2 -2
  49. package/onboarding/prompts/framework/langgraph-python.md +2 -2
  50. package/onboarding/prompts/framework/langgraph-typescript.md +2 -2
  51. package/onboarding/prompts/framework/llamaindex.md +2 -2
  52. package/onboarding/prompts/framework/mastra.md +2 -2
  53. package/onboarding/prompts/framework/ms-agent-dotnet.md +2 -2
  54. package/onboarding/prompts/framework/ms-agent-harness-dotnet.md +2 -2
  55. package/onboarding/prompts/framework/ms-agent-python.md +2 -2
  56. package/onboarding/prompts/framework/pydantic-ai.md +2 -2
  57. package/onboarding/prompts/framework/strands-python.md +2 -2
  58. package/onboarding/prompts/framework/strands-typescript.md +2 -2
  59. package/onboarding/prompts/frontend/angular.md +3 -3
  60. package/onboarding/prompts/frontend/nextjs.md +3 -3
  61. package/onboarding/prompts/frontend/plan.md +9 -8
  62. package/onboarding/prompts/frontend/react-native.md +2 -2
  63. package/onboarding/prompts/frontend/react-spa.md +2 -2
  64. package/onboarding/prompts/frontend/vue.md +2 -2
  65. package/onboarding/prompts/implementation/build-and-validate.md +19 -20
  66. package/onboarding/prompts/proof/complete.md +11 -11
  67. package/onboarding/prompts/proof/oss-baseline.md +5 -5
  68. package/onboarding/prompts/proof/round-trip.md +16 -15
  69. package/onboarding/prompts/research/gather.md +6 -6
  70. package/onboarding/prompts/research/merge.md +3 -3
  71. package/onboarding/prompts/research/preflight.md +4 -4
  72. package/onboarding/prompts/research/route.md +5 -5
  73. package/onboarding/prompts/starter/clone.md +8 -7
  74. package/onboarding/prompts/stopped/run-failed.md +4 -4
  75. package/onboarding/prompts/subagent/create-plan.md +10 -1
  76. package/onboarding/prompts/subagent/inspect-repository.md +9 -2
  77. package/onboarding/prompts/subagent/prove-oss-baseline.md +1 -1
  78. package/onboarding/prompts/subagent/prove-round-trip.md +47 -17
  79. package/onboarding/prompts/unsupported/no-validated-path.md +4 -4
  80. package/package.json +1 -1
  81. package/release/release-tool.js +19 -5
package/README.md CHANGED
@@ -343,7 +343,9 @@ Its first prompt records the coding agent with `onboard identify`, including whe
343
343
  the entry command omitted `--coding-agent`. The agent also passes `--model` with
344
344
  the exact model id its instructions name, and leaves it out when they name none.
345
345
  The event records a missing id as `unknown`, and the coding agent the environment
346
- shows as `detected_coding_agent`. Channel setup stage events include
346
+ shows as `detected_coding_agent`. `cli.onboarding.started` also carries
347
+ `detected_coding_agent`, because a run that stops before `onboard identify` sends
348
+ no identify event. Channel setup stage events include
347
349
  the existing `onboarding_run_id` when the command runs inside an onboarding run;
348
350
  standalone commands omit it. These events use the CLI's existing telemetry consent gate.
349
351
 
@@ -509,6 +511,23 @@ between local and remote access so repeat imports deduplicate. Import while the
509
511
  source is idle. This reads existing history and pending data; it does not execute
510
512
  models, tools or native resume operations.
511
513
 
514
+ ## Import Strands Python History
515
+
516
+ Run `copilotkit import --source strands-python --dry-run` to preview local
517
+ Strands Python sessions. Both `FileSessionManager` records and
518
+ `SnapshotSessionManager` snapshots are supported. Set the source root, stable
519
+ source identity and explicit format through the
520
+ [`STRANDS_PY_IMPORT_*` variables](../../libs/import-strands-python/README.md).
521
+ Omit `--dry-run` to use the normal agent mapping and upload flow.
522
+
523
+ Import an idle store. Extraction reads native files without creating an agent,
524
+ calling a model or tool, or restoring a session. Snapshot imports use the latest
525
+ saved view; they do not merge older snapshots into an invented transcript.
526
+ Unavailable event timestamps remain absent. Native state and pending interaction
527
+ identity are retained; routing a continuation to the original agent is separate.
528
+ Use `ag-ui-strands>=0.4.1` when generating new sources that include frontend-tool
529
+ results. Import cannot repair placeholders already saved by older adapters.
530
+
512
531
  ## Managed Channels
513
532
 
514
533
  `copilotkit channels` connects a hosted Intelligence project to Slack or
@@ -903,6 +922,20 @@ exist yet. After the credential step creates it, run
903
922
  Later changes still fail the audit. The same rule applies to
904
923
  `.copilotkit/project.json`. Missing source files remain protected.
905
924
 
925
+ Inside a git repository, the first `onboard protect` records the files that git
926
+ lists as changed or untracked. Git can restore committed files, so the baseline
927
+ leaves them out. Outside a repository, the baseline records every file under the
928
+ run's folder, except in `node_modules`, `dist`, `build`, `out`, `coverage`,
929
+ `.next`, `venv` and `__pycache__`. The capture report says which of the two it
930
+ used.
931
+
932
+ A git repository at your home directory, or above it, does not count as the
933
+ repository of a project below it. It is usually a dotfiles or accidental
934
+ repository, and `git status` there reads your whole home directory. A run that
935
+ starts in such a project takes the file baseline instead, and the report names
936
+ the repository it did not use. A run that starts in the home directory itself
937
+ still uses its repository.
938
+
906
939
  ## Version Control
907
940
 
908
941
  Outside a repository, `copilotkit init` runs `git init` in the new app and makes
@@ -912,7 +945,9 @@ before you start editing.
912
945
  Inside a repository you already have, it does neither. A second `git init` would
913
946
  make the new app a nested repository: commits made in it would go to the inner
914
947
  repository, while the outer one saw only an untracked directory. The CLI names
915
- the repository it found and leaves the new app for you to commit.
948
+ the repository it found and leaves the new app for you to commit. A repository
949
+ at your home directory, or above it, does not count here, so an app created below
950
+ it gets its own `git init`.
916
951
 
917
952
  The project binding is recorded at that repository's root, so every directory in
918
953
  it resolves to the same project, and the run says where it went. If the root
@@ -1057,9 +1092,10 @@ checks.
1057
1092
  `verify` works the port out from the project rather than assuming one. In order:
1058
1093
 
1059
1094
  1. `--runtime-url`, when you pass it.
1060
- 2. `"runtimeUrl"` in `.copilotkit/project.json` — an absolute URL you add by
1061
- hand for a project whose runtime is somewhere none of the below can find it.
1062
- The CLI never writes this field and never removes one you wrote.
1095
+ 2. `"runtimeUrl"` in `.copilotkit/project.json` — an absolute URL for a
1096
+ project whose runtime is somewhere none of the below can find it. Add it by
1097
+ hand, or let `project select --runtime-url <url>` or
1098
+ `onboard runtime-url --url <url>` write it. The CLI never removes one.
1063
1099
  3. `COPILOTKIT_RUNTIME_URL` in the project's `.env` / `.env.local`, when it is
1064
1100
  absolute.
1065
1101
  4. The port the app's own dev configuration declares: `PORT` in its env files,
@@ -1235,6 +1271,28 @@ started, and a starter that serves both halves from one script loses both when
1235
1271
  either one goes down. Restart what you started, with the command you started it
1236
1272
  with, and leave the rest running.
1237
1273
 
1274
+ ### `verify` probes port 3000 while the app runs on another port
1275
+
1276
+ A dev server whose port is taken moves to the next free one without asking. A
1277
+ project that `init --create` made records no runtime URL, so `verify` assumes
1278
+ port 3000. Record the URL the server reports once it is up:
1279
+
1280
+ ```bash
1281
+ copilotkit onboard runtime-url --url http://localhost:3001/api/copilotkit
1282
+ ```
1283
+
1284
+ It writes `runtimeUrl` into the `.copilotkit/project.json` that governs the
1285
+ current directory and changes nothing else. It mints no key and does not touch
1286
+ `.env`, unlike `project select --runtime-url`, which selects the project again.
1287
+ It needs no bound run. The URL must name the mount path: a bare
1288
+ `http://localhost:3001` is refused, because `verify` would probe `/`. With no
1289
+ project record, it writes nothing and exits non-zero.
1290
+
1291
+ An onboarding run protects the record once it selects the project. When the
1292
+ bound run protects it, the command moves that baseline with its own write, so
1293
+ the next `onboard audit` still passes. It refuses a record that changed since
1294
+ the baseline, so a write it did not make is never absorbed.
1295
+
1238
1296
  ### A model key has no credits or is rejected
1239
1297
 
1240
1298
  A key with no credits passes every static check. The first model call then
@@ -1264,6 +1322,43 @@ SDK at another endpoint, or when the shell exports a different value for the
1264
1322
  key. It never prints the key, and it never prints the vendor's error message,
1265
1323
  because a vendor can echo part of the key in it.
1266
1324
 
1325
+ ### An onboarding run stops while it selects the project
1326
+
1327
+ The onboarding graph settles the project, its key, and the Learning Container
1328
+ with one step. Run it from the app directory whose `.env` receives the key:
1329
+
1330
+ ```bash
1331
+ copilotkit onboard credentials --json
1332
+ copilotkit onboard credentials --create billing-portal \
1333
+ --runtime-url http://localhost:3000/api/copilotkit --json
1334
+ ```
1335
+
1336
+ With no project flag, the step reuses a project that this directory already
1337
+ binds with a key. Otherwise it asks for the project name. With `--create` or
1338
+ `--project`, it runs `project select`, retries once when no key was written, and
1339
+ checks the record and the key. Then it reads the default Learning Container.
1340
+
1341
+ Read `status`:
1342
+
1343
+ - `ready`: the project, key, and container are settled.
1344
+ - `needs-developer`: `question` names the project name or the app directory
1345
+ that the step needs.
1346
+ - `stopped`: `stopReason` names the check, the exact file or code, and the
1347
+ friction category for the stop report.
1348
+
1349
+ When the project is settled, the same step checks the model credential:
1350
+
1351
+ ```bash
1352
+ copilotkit onboard credentials --model-key OPENAI_API_KEY --json
1353
+ ```
1354
+
1355
+ It asks the vendor about each key, as `onboard model-key` does. When every key
1356
+ settles, it protects the credential paths for the run's audit. A missing or
1357
+ refused key is a `needs-developer` answer.
1358
+
1359
+ It needs a bound run and exits zero for every answer. A `stopped` answer files
1360
+ the stop report itself. It never prints a key.
1361
+
1267
1362
  ### A command, subcommand, or flag that should exist is rejected
1268
1363
 
1269
1364
  `npx` keys its cache on the spec string, so `copilotkit@latest` can keep serving
@@ -1409,3 +1504,30 @@ The destination must not already exist. The CLI validates every bundle before it
1409
1504
  If a download or validation fails, the CLI leaves no partial destination.
1410
1505
  Container IDs must be unique and belong to the selected project. Select up to 50 containers.
1411
1506
  Multiple containers use one product API batch request. If any container fails, the whole request fails.
1507
+
1508
+ ## Import standalone LangGraph history
1509
+
1510
+ Use `copilotkit import --source langgraph --langgraph-snapshot native.jsonl`
1511
+ for a Python FastAPI application that does not expose LangGraph Server APIs.
1512
+ Export saved checkpoints with the helper shipped at
1513
+ `exporters/langgraph/export_checkpointer.py` in this CLI package (with its guide
1514
+ at `exporters/langgraph/README.md`). Supply explicit
1515
+ native thread/graph selections when graphs share storage. See the
1516
+ [LangGraph exporter guide](../../libs/import-langgraph/README.md) for the native
1517
+ contract, async export example, discovery rules, source limitations and validation.
1518
+ The existing API credentials, agent map, dry-run and replace options apply.
1519
+ The snapshot retains native IDs and checkpoint provenance; replay does not prove
1520
+ continuation on the original native session.
1521
+
1522
+ ## Replay an application’s custom A2UI catalog
1523
+
1524
+ When native `render_a2ui` calls omit `catalogId`, supply the original application catalog explicitly:
1525
+
1526
+ ```sh
1527
+ copilotkit import --source langgraph --langgraph-snapshot native.jsonl \
1528
+ --agent-map agent-map.json --a2ui-default-catalog-id copilotkit://app-dashboard-catalog -y
1529
+ ```
1530
+
1531
+ The destination frontend must register that catalog. This option works with all import sources and applies to every selected conversation; import applications with different defaults separately. Explicit native `catalogId` values and native A2UI operation payloads take precedence. Native tool arguments/results are preserved; the setting only controls reconstructed UI. Without the option, existing replay behavior is unchanged. Invalid empty/whitespace identifiers fail before upload. No catalog is inferred from message prose.
1532
+
1533
+ API callers can set `replayConfig: { defaultCatalogId: "copilotkit://app-dashboard-catalog" }` on each normalized conversation. An unchanged source identity is still deduplicated: changing this option does not update an existing import. Use the existing `--replace` workflow to regenerate its imported history, after accounting for any destination changes.
@@ -2,20 +2,20 @@
2
2
  "schemaVersion": 1,
3
3
  "package": {
4
4
  "name": "copilotkit",
5
- "version": "4.17.0"
5
+ "version": "4.18.0"
6
6
  },
7
7
  "intelligence": {
8
- "commit": "f6f91f8357a9058dcda3f2ef07a3283e23ddb9e8"
8
+ "commit": "3a9266b5fa673a4974ad204d131ed63d5696dc00"
9
9
  },
10
10
  "copilotKit": {
11
- "submittedInput": "0c8647a7f4ec",
12
- "commit": "0c8647a7f4ecf565e58405642539c9d9744f62c5"
11
+ "submittedInput": "70d1f3c0a832",
12
+ "commit": "70d1f3c0a832d6274e2fe710e24102f6429f14f4"
13
13
  },
14
14
  "channel": "production",
15
15
  "triggeringActor": "BenTaylorDev",
16
16
  "workflow": {
17
- "runId": "36178536102",
18
- "runUrl": "https://github.com/CopilotKit/Intelligence/actions/runs/36178536102"
17
+ "runId": "36480955998",
18
+ "runUrl": "https://github.com/CopilotKit/Intelligence/actions/runs/36480955998"
19
19
  },
20
20
  "validationResult": "passed",
21
21
  "ag2": {
@@ -24,5 +24,5 @@
24
24
  "revision": "main",
25
25
  "pinned": false
26
26
  },
27
- "builtAt": "2026-09-25T19:17:42Z"
27
+ "builtAt": "2026-09-28T20:43:08Z"
28
28
  }
@@ -0,0 +1,118 @@
1
+ # LangGraph import
2
+
3
+ `@cpki/import-langgraph` normalizes saved LangGraph Server history and standalone
4
+ Python graph checkpoint exports for the shared Intelligence import pipeline.
5
+
6
+ ## Standalone FastAPI applications
7
+
8
+ FastAPI and AG-UI do not imply a LangGraph Server thread API. Use the compiled
9
+ graph and its configured checkpointer in the application's own environment.
10
+ The exporter does not open SQL tables, invoke graph nodes, answer interrupts,
11
+ or write checkpoints. It requires public `aget_state_history`, `aget_state`,
12
+ `get_output_jsonschema`, and optionally the saver's `alist` API.
13
+
14
+ Copy `python/export_checkpointer.py` into your application tooling (also shipped
15
+ in the CLI package under `exporters/langgraph/`). Run this with the application's
16
+ existing dependency versions and matching compiled graph:
17
+
18
+ ```python
19
+ import json
20
+ from export_checkpointer import export_thread
21
+
22
+ # graph already has the application's durable checkpointer attached.
23
+ # Obtain this selection from your application's thread/graph registry.
24
+ configs = [{"configurable": {"thread_id": "native-thread-id", "checkpoint_ns": ""}}]
25
+ with open("native.jsonl", "x", encoding="utf-8") as output:
26
+ for config in configs:
27
+ record = await export_thread(
28
+ graph, config, source_id="production-chat-store", graph_id="chat"
29
+ )
30
+ output.write(json.dumps(record, allow_nan=False) + "\n")
31
+ ```
32
+
33
+ This snippet belongs inside your application's async export routine with the
34
+ checkpointer open. Use an idle source or a consistent read-only database copy.
35
+ The exporter detects a changed head checkpoint and asks you to retry. It cannot
36
+ provide a transaction spanning arbitrary custom storage implementations. Never
37
+ run startup code that initializes or mutates the retained production store merely
38
+ to export it. Preserve the same source label when exporting a copy of that store.
39
+
40
+ If one graph owns the entire selected namespace, discover configs with
41
+ `async for config in discover_threads(saver, graph_owns_namespace=True)`.
42
+ `checkpoint_ns` defaults to the root namespace `""`; select another namespace
43
+ explicitly with a compiled graph that supports reading it. Do not infer ownership
44
+ when multiple graphs share the same saver and namespace. Supply explicit configs
45
+ from the application's registry for each graph instead. Missing graph ownership
46
+ cannot be recovered from the generic saver contract. Stores without discovery
47
+ support can still export known thread IDs.
48
+
49
+ ```sh
50
+ copilotkit import --source langgraph --langgraph-snapshot native.jsonl \
51
+ --agent-map agent-map.json --unrecoverable imported-unknown --dry-run -y
52
+ ```
53
+
54
+ `agent-map.json` maps the declared graph key to your configured Intelligence agent,
55
+ for example `{"chat":"beautiful-chat"}`. Remove `--dry-run` after inspecting the
56
+ preview. Existing API credentials and `--replace` behavior apply. Snapshot mode
57
+ makes no LangGraph Server requests. Invalid records, conflicting graph identity,
58
+ duplicate conversation identities, and histories that do not follow a newest-first
59
+ parent checkpoint chain fail enumeration before opening a batch.
60
+ Treat the file as sensitive conversation data; it may include user messages,
61
+ attachments, graph state and interrupt values. No database credentials or runtime
62
+ configuration are exported by the helper.
63
+
64
+ ## Fidelity and identity
65
+
66
+ The current checkpoint's messages and output-schema-projected state are
67
+ canonical. Available text, media, tool calls/results, frontend tool requests and
68
+ native task interrupts reuse the existing converter and replay pipeline. The
69
+ exporter uses the head read to apply completed pending writes. It preserves
70
+ native task/interrupt IDs and checkpoint/parent selectors as observed provenance.
71
+ Exports follow the current head’s ancestor chain; sibling branches are excluded.
72
+ Those selectors never authorize a rewind, fork or outbound route.
73
+
74
+ The opaque dedup key is a JSON tuple of format, source ID, graph ID, namespace and
75
+ native thread ID. It is stable across checkpoint revisions and distinct across
76
+ stores/namespaces. It is not an executable thread ID. `nativeSource.threadId`
77
+ retains the exact native ID. `sourceId` is a stable non-secret store/deployment
78
+ label; `graphId` selects interpretation and does not prove physical isolation.
79
+ Continuation to the original session is separate from replay/import support.
80
+
81
+ Checkpointers do not supply Server run records. Imported transcripts therefore
82
+ use a synthetic run with unknown status unless a native failure is recorded;
83
+ metadata explicitly marks run boundaries unavailable. Middleware-only UI or HTML
84
+ never saved by the source cannot be reconstructed. Unknown non-JSON native values
85
+ and integers outside the JavaScript safe range fail export instead of silently
86
+ changing their values. Task exceptions are exported as their Python `repr`
87
+ (type and message), matching persisted LangGraph task errors.
88
+
89
+ ## Tests
90
+
91
+ ```sh
92
+ python -m pip install -r libs/import-langgraph/python/requirements-test.txt
93
+ pnpm nx run @cpki/import-langgraph:test-native
94
+ pnpm nx test @cpki/import-langgraph
95
+ ```
96
+
97
+ Install the workspace Node dependencies with `pnpm install --frozen-lockfile`
98
+ before running the native tests. They emit JSON Schema from the actual TypeScript
99
+ importer schema and validate every fresh export against it.
100
+
101
+ The SQLite test runs a local graph with a genuine interrupt, checks checkpoint
102
+ and pending-write rows before/after export, reopens the database and compares
103
+ snapshot equality and database bytes. It makes no model/provider calls. The
104
+ captured synthetic native snapshot in `fixtures/langgraph/checkpointer-native.jsonl`
105
+ is consumed by TypeScript contract tests.
106
+
107
+ ## Replay an application’s custom A2UI catalog
108
+
109
+ When native `render_a2ui` calls omit `catalogId`, supply the original application catalog explicitly:
110
+
111
+ ```sh
112
+ copilotkit import --source langgraph --langgraph-snapshot native.jsonl \
113
+ --agent-map agent-map.json --a2ui-default-catalog-id copilotkit://app-dashboard-catalog -y
114
+ ```
115
+
116
+ The destination frontend must register that catalog. This option works with all import sources and applies to every selected conversation; import applications with different defaults separately. Explicit native `catalogId` values and native A2UI operation payloads take precedence. Native tool arguments/results are preserved; the setting only controls reconstructed UI. Without the option, existing replay behavior is unchanged. Invalid empty/whitespace identifiers fail before upload. No catalog is inferred from message prose.
117
+
118
+ API callers can set `replayConfig: { defaultCatalogId: "copilotkit://app-dashboard-catalog" }` on each normalized conversation. An unchanged source identity is still deduplicated: changing this option does not update an existing import. Use the existing `--replace` workflow to regenerate its imported history, after accounting for any destination changes.
@@ -0,0 +1,125 @@
1
+ """Read-only LangGraph export helpers. Run inside the application's Python environment.
2
+
3
+ The caller owns checkpointer lifecycle, credentials and graph/thread selection.
4
+ This module never invokes a graph or opens a database directly.
5
+ """
6
+ from dataclasses import asdict, is_dataclass
7
+ from math import isfinite
8
+
9
+ from langchain_core.messages import BaseMessage
10
+
11
+
12
+ def json_value(value):
13
+ """Convert native messages/tasks into JSON without lossy string fallbacks."""
14
+ if value is None or isinstance(value, (str, bool)):
15
+ return value
16
+ if isinstance(value, int):
17
+ if abs(value) > 9007199254740991:
18
+ raise ValueError('Native integer exceeds the JavaScript safe integer range')
19
+ return value
20
+ if isinstance(value, float) and isfinite(value):
21
+ return value
22
+ if isinstance(value, Exception):
23
+ # Match LangGraph's persisted task-error representation (type + message).
24
+ return repr(value)
25
+ if isinstance(value, BaseMessage):
26
+ return json_value(value.model_dump(mode='json'))
27
+ if is_dataclass(value) and not isinstance(value, type):
28
+ return json_value(asdict(value))
29
+ if hasattr(value, '_asdict'):
30
+ return json_value(value._asdict())
31
+ if isinstance(value, dict):
32
+ if any(not isinstance(key, str) for key in value):
33
+ raise ValueError('Native data contains non-string JSON object keys')
34
+ return {key: json_value(item) for key, item in value.items()}
35
+ if isinstance(value, (list, tuple)):
36
+ return [json_value(item) for item in value]
37
+ raise ValueError(f'Native data is not losslessly JSON serializable: {type(value).__name__}')
38
+
39
+
40
+ def checkpoint_ref(config):
41
+ """Keep checkpoint selectors only; never export source credentials/configuration."""
42
+ if config is None:
43
+ return None
44
+ selected = config['configurable']
45
+ return {key: selected[key] for key in ('thread_id', 'checkpoint_ns', 'checkpoint_id', 'checkpoint_map') if key in selected}
46
+
47
+
48
+ async def discover_threads(checkpointer, *, graph_owns_namespace, checkpoint_ns=''):
49
+ """Discover IDs via public saver API in a namespace owned by ONE graph.
50
+
51
+ Do not use on a store shared by graphs without a separate ownership registry.
52
+ Instead supply explicit per-graph thread configs from that registry.
53
+ """
54
+ if graph_owns_namespace is not True:
55
+ raise ValueError('Discovery requires explicit single-graph namespace ownership; supply selected configs for shared stores')
56
+ seen = set()
57
+ async for item in checkpointer.alist(None):
58
+ selected = item.config['configurable']
59
+ if selected.get('checkpoint_ns', '') != checkpoint_ns:
60
+ continue
61
+ thread_id = selected['thread_id']
62
+ if thread_id not in seen:
63
+ seen.add(thread_id)
64
+ yield {'configurable': {'thread_id': thread_id, 'checkpoint_ns': checkpoint_ns}}
65
+
66
+
67
+ async def export_thread(graph, config, *, source_id, graph_id):
68
+ """Export one selected native thread through public graph history reads.
69
+
70
+ Use an idle source or a consistent read-only copy and the matching compiled
71
+ graph. History is the current head's ancestry, newest-first, including pending
72
+ task writes/interrupts. Sibling branches are excluded.
73
+ A checkpoint revision in config is rejected: this exports the current head,
74
+ not a branch rewind. JSON output is data, never continuation authorization.
75
+ """
76
+ if not source_id or not graph_id:
77
+ raise ValueError('source_id and graph_id must be nonempty stable labels')
78
+ selected = config['configurable']
79
+ if not selected.get('thread_id') or selected.get('checkpoint_id'):
80
+ raise ValueError('Select a native thread ID without a checkpoint revision')
81
+ namespace = selected.get('checkpoint_ns', '')
82
+ history = []
83
+ async for state in graph.aget_state_history(config):
84
+ if state.metadata and state.metadata.get('graph_id', graph_id) != graph_id:
85
+ raise ValueError('Recorded graph ownership conflicts with selected graph')
86
+ history.append({
87
+ 'values': json_value(state.values),
88
+ 'next': list(state.next),
89
+ 'tasks': json_value(state.tasks),
90
+ 'checkpoint': checkpoint_ref(state.config),
91
+ 'parent_checkpoint': checkpoint_ref(state.parent_config),
92
+ 'metadata': json_value(state.metadata),
93
+ 'created_at': state.created_at,
94
+ })
95
+ if not history:
96
+ raise ValueError('No saved checkpoints for the selected native thread')
97
+ # Detect a concurrent source advance instead of claiming a consistent export.
98
+ head = await graph.aget_state(config)
99
+ if checkpoint_ref(head.config) != history[0]['checkpoint']:
100
+ raise ValueError('Native checkpoint advanced during export; retry on an idle source')
101
+ # aget_state applies completed pending writes to the authoritative head.
102
+ history[0]['values'] = json_value(head.values)
103
+ history[0]['next'] = list(head.next)
104
+ history[0]['tasks'] = json_value(head.tasks)
105
+ # Public history can contain sibling branches after a fork. Export only the
106
+ # current head's ancestor chain so adjacency has the same meaning on import.
107
+ revisions = {entry['checkpoint']['checkpoint_id']: entry for entry in history}
108
+ lineage = []
109
+ seen = set()
110
+ entry = history[0]
111
+ while entry is not None:
112
+ revision = entry['checkpoint']['checkpoint_id']
113
+ if revision in seen:
114
+ raise ValueError('Native checkpoint ancestry contains a cycle')
115
+ seen.add(revision)
116
+ lineage.append(entry)
117
+ parent = entry['parent_checkpoint']
118
+ entry = revisions.get(parent['checkpoint_id']) if parent else None
119
+ return {
120
+ 'format': 'copilotkit.langgraph.checkpointer.v1',
121
+ 'sourceId': source_id, 'graphId': graph_id,
122
+ 'config': {'thread_id': selected['thread_id'], 'checkpoint_ns': namespace},
123
+ 'outputSchema': json_value(graph.get_output_jsonschema()),
124
+ 'history': lineage,
125
+ }