@braincrew-lab/langchain-canvas 0.3.0 → 0.4.9

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.
@@ -0,0 +1,68 @@
1
+ import { C as CanvasTransport, E as ElementSelection, S as StreamEvent } from '../types-BfGP9R2I.js';
2
+
3
+ /**
4
+ * `langgraphTransport` — speak to a LangGraph server without a translator
5
+ * in between.
6
+ *
7
+ * Built on the official `@langchain/langgraph-sdk` (the server's wire format
8
+ * belongs to LangGraph and evolves with it — we ride the official client
9
+ * rather than hand-parse it). Verified against `langgraph dev` (local);
10
+ * hosted LangGraph Platform is untested — open an issue if you need it.
11
+ *
12
+ * Per user turn it: maps the canvas thread id to the UUID LangGraph requires,
13
+ * makes sure the thread exists, frames any element selections into the
14
+ * message (targeted edits), then streams the run with
15
+ * `streamMode: ["messages-tuple", "custom"]` through the translation in
16
+ * `translate.ts`.
17
+ */
18
+
19
+ interface LangGraphTransportOptions {
20
+ /** LangGraph server URL, e.g. `http://127.0.0.1:2024` (`langgraph dev`). */
21
+ url: string;
22
+ /** Graph/assistant to run, e.g. `"canvas_agent"`. */
23
+ assistantId: string;
24
+ /** Extra headers (e.g. auth) passed to the SDK client. */
25
+ headers?: Record<string, string>;
26
+ }
27
+ /** Frame a targeted edit so the agent changes only the selected element(s). */
28
+ declare function withSelections(message: string, selections: ElementSelection[]): string;
29
+ /**
30
+ * LangGraph requires UUID thread ids; the canvas allows any string. Non-UUID
31
+ * ids map deterministically (RFC 4122 v5 over `canvas-thread:<id>`), matching
32
+ * the mapping the Python bridge example uses — same id in, same UUID out.
33
+ */
34
+ declare function threadUuid(threadId: string): Promise<string>;
35
+ declare function langgraphTransport(options: LangGraphTransportOptions): CanvasTransport;
36
+
37
+ /**
38
+ * Translate a LangGraph run stream into Canvas Wire Protocol events.
39
+ *
40
+ * Input: the `{event, data}` chunks the LangGraph SDK yields for a run
41
+ * streamed with `streamMode: ["messages-tuple", "custom"]`. Output: the
42
+ * `StreamEvent`s the canvas applies. The mapping:
43
+ *
44
+ * - `messages` AIMessageChunk text → `message.delta`
45
+ * - `messages` AIMessageChunk tool chunks → `tool.start` (once per call id)
46
+ * - `messages` tool result → `tool.end`
47
+ * - `custom` `canvas.*` → passed through untouched
48
+ * - `error` → `error`
49
+ * - stream end → `message.end` + `done`
50
+ *
51
+ * The chunk shapes are pinned by a captured fixture from a real
52
+ * `langgraph dev` run (`__fixtures__/langgraph-run.json`) — notably, model
53
+ * content arrives as block arrays (`{type: "text" | "tool_use"}`), not plain
54
+ * strings.
55
+ */
56
+
57
+ /** One chunk from `client.runs.stream(...)` — the SDK's `{event, data}` pair. */
58
+ interface LangGraphStreamChunk {
59
+ event: string;
60
+ data: unknown;
61
+ }
62
+ /** Text of a message chunk — models may stream content as block lists. */
63
+ declare function chunkText(content: unknown): string;
64
+ declare function translateLangGraphStream(chunks: AsyncIterable<LangGraphStreamChunk> | Iterable<LangGraphStreamChunk>, options?: {
65
+ messageId?: string;
66
+ }): AsyncGenerator<StreamEvent>;
67
+
68
+ export { type LangGraphStreamChunk, type LangGraphTransportOptions, chunkText, langgraphTransport, threadUuid, translateLangGraphStream, withSelections };
@@ -0,0 +1,111 @@
1
+ import { Client } from '@langchain/langgraph-sdk';
2
+
3
+ // src/langgraph/transport.ts
4
+
5
+ // src/langgraph/translate.ts
6
+ function chunkText(content) {
7
+ if (typeof content === "string") return content;
8
+ if (Array.isArray(content)) {
9
+ return content.filter(
10
+ (block) => typeof block === "object" && block !== null && block.type === "text" && typeof block.text === "string"
11
+ ).map((block) => block.text).join("");
12
+ }
13
+ return "";
14
+ }
15
+ function isRecord(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+ async function* translateLangGraphStream(chunks, options = {}) {
19
+ const messageId = options.messageId ?? `msg_${Math.random().toString(36).slice(2, 14)}`;
20
+ const startedTools = /* @__PURE__ */ new Set();
21
+ for await (const chunk of chunks) {
22
+ if (chunk.event === "error") {
23
+ const detail = typeof chunk.data === "string" ? chunk.data : JSON.stringify(chunk.data);
24
+ yield { type: "error", message: `agent run failed: ${detail}` };
25
+ continue;
26
+ }
27
+ if (chunk.event === "custom") {
28
+ if (isRecord(chunk.data) && String(chunk.data.type ?? "").startsWith("canvas.")) {
29
+ yield chunk.data;
30
+ }
31
+ continue;
32
+ }
33
+ if (chunk.event !== "messages" || !Array.isArray(chunk.data) || chunk.data.length === 0) {
34
+ continue;
35
+ }
36
+ const msg = chunk.data[0];
37
+ if (!isRecord(msg)) continue;
38
+ const meta = chunk.data.length > 1 && isRecord(chunk.data[1]) ? chunk.data[1] : {};
39
+ const fromToolsNode = meta.langgraph_node === "tools";
40
+ if (msg.type === "AIMessageChunk") {
41
+ const calls = Array.isArray(msg.tool_call_chunks) ? msg.tool_call_chunks : [];
42
+ for (const call of calls) {
43
+ if (!isRecord(call)) continue;
44
+ const id = typeof call.id === "string" ? call.id : null;
45
+ const name = typeof call.name === "string" ? call.name : null;
46
+ if (id && name && !startedTools.has(id)) {
47
+ startedTools.add(id);
48
+ yield { type: "tool.start", toolCallId: id, name };
49
+ }
50
+ }
51
+ const text = chunkText(msg.content);
52
+ if (text && !fromToolsNode) yield { type: "message.delta", messageId, text };
53
+ } else if (msg.type === "tool") {
54
+ const id = typeof msg.tool_call_id === "string" ? msg.tool_call_id : null;
55
+ if (id) yield { type: "tool.end", toolCallId: id, ok: msg.status !== "error" };
56
+ }
57
+ }
58
+ yield { type: "message.end", messageId };
59
+ yield { type: "done" };
60
+ }
61
+
62
+ // src/langgraph/transport.ts
63
+ function withSelections(message, selections) {
64
+ if (selections.length === 0) return message;
65
+ const listed = selections.map((s) => `- \`${s.selector}\` (data-cid=${s.cid})`).join("\n");
66
+ const artifactId = selections[0].artifactId;
67
+ return `${message}
68
+
69
+ [Targeted edit] Apply the change to these selected element(s) in file \`${artifactId}\`:
70
+ ${listed}
71
+ First call read_canvas on the file to get its current content and revision, then call edit_canvas with the element's exact current outer HTML as \`old\` and your replacement as \`new\` (keep the data-cid attribute).`;
72
+ }
73
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
74
+ var NAMESPACE_URL = "6ba7b811-9dad-11d1-80b4-00c04fd430c8";
75
+ async function threadUuid(threadId) {
76
+ if (UUID_RE.test(threadId)) return threadId.toLowerCase();
77
+ const name = new TextEncoder().encode(`canvas-thread:${threadId}`);
78
+ const namespace = NAMESPACE_URL.replace(/-/g, "");
79
+ const namespaceBytes = new Uint8Array(16);
80
+ for (let i = 0; i < 16; i++) namespaceBytes[i] = parseInt(namespace.slice(i * 2, i * 2 + 2), 16);
81
+ const payload = new Uint8Array(namespaceBytes.length + name.length);
82
+ payload.set(namespaceBytes);
83
+ payload.set(name, namespaceBytes.length);
84
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-1", payload)).slice(0, 16);
85
+ digest[6] = digest[6] & 15 | 80;
86
+ digest[8] = digest[8] & 63 | 128;
87
+ const hex = [...digest].map((b) => b.toString(16).padStart(2, "0")).join("");
88
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
89
+ }
90
+ function langgraphTransport(options) {
91
+ const client = new Client({ apiUrl: options.url, defaultHeaders: options.headers });
92
+ const knownThreads = /* @__PURE__ */ new Set();
93
+ return {
94
+ async *stream(request) {
95
+ const threadId = await threadUuid(request.threadId);
96
+ if (!knownThreads.has(threadId)) {
97
+ await client.threads.create({ threadId, ifExists: "do_nothing" });
98
+ knownThreads.add(threadId);
99
+ }
100
+ const message = withSelections(request.message, request.selections ?? []);
101
+ const chunks = client.runs.stream(threadId, options.assistantId, {
102
+ input: { messages: [{ role: "user", content: message }] },
103
+ streamMode: ["messages-tuple", "custom"],
104
+ signal: request.signal
105
+ });
106
+ yield* translateLangGraphStream(chunks);
107
+ }
108
+ };
109
+ }
110
+
111
+ export { chunkText, langgraphTransport, threadUuid, translateLangGraphStream, withSelections };