diff --git a/cmd/oc/internal/commands/agent_crud.go b/cmd/oc/internal/commands/agent_crud.go index 2c9015a9..30aaf1f0 100644 --- a/cmd/oc/internal/commands/agent_crud.go +++ b/cmd/oc/internal/commands/agent_crud.go @@ -120,7 +120,7 @@ var agentGetCmd = &cobra.Command{ func registerAgentCrud() { agentCreateCmd.Flags().String("prompt", "", "System prompt (required, except --runtime flue)") agentCreateCmd.Flags().String("model", "", "Model, e.g. anthropic/claude-sonnet-5 (required)") - agentCreateCmd.Flags().String("runtime", "claude", "Runtime family (claude|codex|pi|flue)") + agentCreateCmd.Flags().String("runtime", "claude", "Runtime family (claude|codex|pi|flue|langgraph)") agentCreateCmd.Flags().String("credential", "", "Credential id (optional)") agentGetCmd.Flags().String("agent", "", "Agent id or name (else the cwd agent.toml)") diff --git a/cmd/oc/internal/commands/agent_deploy.go b/cmd/oc/internal/commands/agent_deploy.go index 56c25327..35dfbce8 100644 --- a/cmd/oc/internal/commands/agent_deploy.go +++ b/cmd/oc/internal/commands/agent_deploy.go @@ -228,6 +228,11 @@ var agentDeployCmd = &cobra.Command{ noActivate, _ := cmd.Flags().GetBool("no-activate") return deployFlue(cmd, sc, dir, m, noActivate) } + // LangGraph agents likewise deploy a built Worker artifact (its own runtime). + if m.Runtime.Family == "langgraph" { + noActivate, _ := cmd.Flags().GetBool("no-activate") + return deployLangGraph(cmd, sc, dir, m, noActivate) + } prompt, err := readPrompt(dir) if err != nil { return fmt.Errorf("read prompt.md: %w", err) diff --git a/cmd/oc/internal/commands/agent_deploy_langgraph.go b/cmd/oc/internal/commands/agent_deploy_langgraph.go new file mode 100644 index 00000000..6e92ee81 --- /dev/null +++ b/cmd/oc/internal/commands/agent_deploy_langgraph.go @@ -0,0 +1,107 @@ +package commands + +// LangGraph deploy flow — the standalone langgraph runtime (model A: the graph runs +// inside a self-hosting Worker + one Durable Object per session). Mirrors the flue +// flow (build client-side, stage the bundle in R2 via presigned PUT, POST a byte-free +// deployment) but is independent of flue: the bundle comes from the project's own +// wrangler, and the deployment carries `langgraph_*` fields. The server registers the +// `langgraph` family, then hosts the Worker + binds its session DO like any WfP Worker. + +import ( + "fmt" + "time" + + "github.com/opensandbox/opensandbox/cmd/oc/internal/client" + "github.com/opensandbox/opensandbox/cmd/oc/internal/langgraphbuild" + "github.com/spf13/cobra" +) + +func deployLangGraph(cmd *cobra.Command, sc *client.Client, dir string, m *manifest, noActivate bool) error { + // 1. Resolve the target agent first (create with runtime=langgraph + no prompt if + // new) so a runtime-family mismatch fails fast, before the (slow) bundle. + id, err := resolveDeployAgentFamily(cmd, sc, m, "langgraph") + if err != nil { + return err + } + // 2. Persist [vars] before enqueueing so the off-host host can't race ahead with + // stale bindings (secrets stay CLI/API-only, resolved by that same host). Only when + // the manifest declares vars — the config endpoint has no state for a fresh agent. + if len(m.Vars) > 0 { + if err := syncManifestVars(cmd, sc, id, m); err != nil { + return err + } + } + + // 3. Bundle the project with its own wrangler → tar.gz + descriptor + digest. + art, err := langgraphbuild.Build(cmd.Context(), dir) + if err != nil { + return err + } + + // 4. Upload: presigned PUT to R2 (the API host never sees the bytes). + if err := uploadArtifact(cmd.Context(), sc, id, art.Digest, art.Bundle); err != nil { + return err + } + + // 5. Byte-free deployment referencing the R2 bundle + the strict descriptor. + rt := m.Runtime.Type + if rt == "" { + rt = "default" + } + // Post the artifact under the langgraph family field names. The server routes runtime=langgraph + // through the same generic self-hosting WfP-DO deploy pipeline flue uses, normalizing + // langgraph_*/flue_* to one enqueue — so these are the honest langgraph wire fields, not a masquerade. + input := map[string]interface{}{ + "type": "inline", + "model": m.Model, + "runtime": map[string]string{"type": rt}, + "langgraph_bundle_digest": art.Digest, + "langgraph_wrangler": art.Wrangler, + "langgraph_entrypoint": art.Entrypoint, // the session Durable Object class = admit address + } + body := map[string]interface{}{"input": input, "activate": !noActivate} + if idem, _ := cmd.Flags().GetString("idempotency-key"); idem != "" { + body["idempotency_key"] = idem + } + var env DeploymentEnvelope + if err := sc.Post(cmd.Context(), "/v3/agents/"+id+"/deployments", body, &env); err != nil { + return err + } + d := env.Deployment + + // 6. Poll to terminal while the off-host host uploads + finalizes. + if !terminalState(d.State) && d.State != "" { + to, _ := cmd.Flags().GetInt("timeout") + d, err = pollDeployment(cmd, sc, id, d.ID, time.Duration(to)*time.Second) + if err != nil { + return err + } + } + if d.State == "failed" { + printer.Print(d, func() { fmt.Printf("Deploy failed: %s\n", deployFailMsg(d)) }) + return &ExitError{Code: 1} + } + printer.Print(d, func() { + n := revisionNumber(cmd, sc, id, d.RevisionID) + status := "staged" + if d.Active { + status = "active" + } + fmt.Printf("Deployed revision %d — %s (%s)\n", n, status, shortDigest(art.Digest)) + }) + return nil +} + +// resolveDeployAgentFamily picks the agent a code-runtime deploy targets: --agent > +// manifest [agent].id > ensure-by-name (creating an agent of the given family, no +// prompt, if absent). Generalizes resolveDeployAgent (which is flue-specific). +func resolveDeployAgentFamily(cmd *cobra.Command, sc *client.Client, m *manifest, family string) (string, error) { + if explicit, _ := cmd.Flags().GetString("agent"); explicit != "" { + return resolveRef(cmd, sc, explicit) + } + if m.Agent.ID != "" { + return m.Agent.ID, nil + } + id, _, err := ensureAgentByName(cmd, sc, m.Name, "", m.Model, family) + return id, err +} diff --git a/cmd/oc/internal/commands/agent_init.go b/cmd/oc/internal/commands/agent_init.go index 596235f9..aed9e635 100644 --- a/cmd/oc/internal/commands/agent_init.go +++ b/cmd/oc/internal/commands/agent_init.go @@ -12,7 +12,7 @@ const agentTomlTmpl = `name = %q model = %q [runtime] -family = %q # claude | codex | pi +family = %q # claude | codex | pi | flue | langgraph type = "default" [limits] @@ -35,12 +35,185 @@ Describe what this skill does and when to use it. Delete the skills/ directory entirely if your agent doesn't need skills. ` +// ── LangGraph (JS) runtime scaffold ────────────────────────────────────────── +// A code runtime (not prompt-based): the agent IS a compiled LangGraph.js graph. +// The scaffold is self-contained and runs locally (npm i && npm run dev); the +// OpenComputer `langgraph` runner drives the exported `graph` per session. + +const lgPackageJSON = `{ + "name": %q, + "private": true, + "type": "module", + "engines": { "node": ">=22.19.0 <23" }, + "scripts": { + "dev": "tsx src/graph.ts", + "typecheck": "tsc --noEmit", + "deploy": "oc agent deploy" + }, + "dependencies": { + "@opencomputer/langgraph": "^0.0.1", + "@langchain/langgraph": "^1.4.8", + "@langchain/langgraph-checkpoint": "^1.0.0", + "@langchain/anthropic": "^1.5.1", + "@langchain/core": "^1.2.3" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "tsx": "^4.20.0", + "typescript": "^5.9.3", + "wrangler": "^4.0.0" + } +} +` + +const lgTsconfig = `{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "types": ["node"] + }, + "include": ["src"] +} +` + +const lgGitignore = `node_modules/ +dist/ +.env +.env.* +*.log +.DS_Store +` + +// src/graph.ts — the StateGraph. THIS is where you author your agent. +const lgGraphTs = `import { StateGraph, MessagesAnnotation, START, END } from "@langchain/langgraph"; +import { HumanMessage } from "@langchain/core/messages"; +import type { BaseCheckpointSaver } from "@langchain/langgraph-checkpoint"; +import { ocModel } from "@opencomputer/langgraph"; + +// THIS is where you author your agent. Nodes call models via ocModel(env): env is +// config.configurable.env on deploy (carries the gateway token) and process.env +// locally. Keep 'compile' exported so the runtime injects the durable per-session +// Durable Object checkpointer. +const builder = new StateGraph(MessagesAnnotation) + .addNode("agent", async (state, config) => { + const env = (config?.configurable?.env as Record) ?? process.env; + const res = await ocModel(env).invoke(state.messages); + return { messages: [res] }; + }) + .addEdge(START, "agent") + .addEdge("agent", END); + +export const compile = (checkpointer: BaseCheckpointSaver) => builder.compile({ checkpointer }); + +// Local smoke test: npm run dev (needs ANTHROPIC_API_KEY). +if (import.meta.url === "file://" + process.argv[1]) { + const { MemorySaver } = await import("@langchain/langgraph"); + const graph = compile(new MemorySaver()); + const out = await graph.invoke( + { messages: [new HumanMessage("Say hello in one sentence.")] }, + { configurable: { thread_id: "local" } }, + ); + console.log((out.messages.at(-1) as { content?: unknown } | undefined)?.content); +} +` + +// src/app.ts — the hosting entry: mounts the standalone langgraph runtime. +const lgAppTs = `// Hosting entry for the OpenComputer langgraph runtime. createLangGraphRuntime wires +// the session transport (POST/GET /agents/:agent/:session + /health) and a per-session +// Durable Object that runs your graph with a durable DO-backed checkpointer. Keep both +// exports; LangGraphSession must match the class_name in wrangler.jsonc. +import { createLangGraphRuntime } from "@opencomputer/langgraph"; +import { compile } from "./graph.js"; + +const runtime = createLangGraphRuntime({ compile }); + +export default { fetch: runtime.fetch }; +export const LangGraphSession = runtime.SessionDO; +` + +// wrangler.jsonc — the Worker + Durable Object binding for deploy. +const lgWrangler = `{ + "name": %q, + "main": "src/app.ts", + "compatibility_date": "2026-07-01", + "compatibility_flags": ["nodejs_compat"], + "durable_objects": { + "bindings": [{ "name": "SESSION", "class_name": "LangGraphSession" }] + }, + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["LangGraphSession"] }] +} +` + +const lgReadme = `# %s — LangGraph (JS) agent + +An OpenComputer agent whose brain is a LangGraph.js ` + "`StateGraph`" + `. + +## Author +Edit ` + "`src/graph.ts`" + ` — add nodes, edges, tools, conditional routing. Model calls +go through ` + "`ocModel(env)`" + ` (from @opencomputer/langgraph), which points a LangChain +Anthropic model at the OpenComputer gateway on deploy and at ANTHROPIC_API_KEY +locally. Keep ` + "`compile`" + ` exported — the runtime injects the durable checkpointer. + +## Run locally + npm install + ANTHROPIC_API_KEY=sk-ant-... npm run dev + +## Deploy + oc agent deploy + +## Hosting + durable state +` + "`src/app.ts`" + ` mounts createLangGraphRuntime — its own session transport plus one +Durable Object per session. Local ` + "`npm run dev`" + ` uses an in-memory MemorySaver; on +deploy the runtime injects a DurableObjectSaver automatically, so graph state +persists and resumes across invocations (Postgres/Redis savers can't run on Workers). +` + +// scaffoldLangGraph writes a self-contained LangGraph.js project into dir. +func scaffoldLangGraph(dir, name, model string) error { + if err := os.MkdirAll(filepath.Join(dir, "src"), 0o755); err != nil { + return err + } + files := []struct{ path, content string }{ + {filepath.Join(dir, "agent.toml"), fmt.Sprintf(agentTomlTmpl, name, model, "langgraph")}, + {filepath.Join(dir, "package.json"), fmt.Sprintf(lgPackageJSON, name)}, + {filepath.Join(dir, "wrangler.jsonc"), fmt.Sprintf(lgWrangler, name)}, + {filepath.Join(dir, "tsconfig.json"), lgTsconfig}, + {filepath.Join(dir, ".gitignore"), lgGitignore}, + {filepath.Join(dir, "README.md"), fmt.Sprintf(lgReadme, name)}, + {filepath.Join(dir, "src", "graph.ts"), lgGraphTs}, + {filepath.Join(dir, "src", "app.ts"), lgAppTs}, + } + return writeScaffold(files, dir, "Edit src/graph.ts, then: npm install && oc agent deploy") +} + +// writeScaffold writes each file (skipping existing), prints progress, and a footer. +func writeScaffold(files []struct{ path, content string }, dir, next string) error { + created := 0 + for _, f := range files { + if _, err := os.Stat(f.path); err == nil { + fmt.Printf(" skip %s (exists)\n", f.path) + continue + } + if err := os.WriteFile(f.path, []byte(f.content), 0o644); err != nil { + return err + } + fmt.Printf(" create %s\n", f.path) + created++ + } + fmt.Printf("\nScaffolded %d file(s). %s\n", created, next) + return nil +} + var agentInitCmd = &cobra.Command{ Use: "init [dir]", - Short: "Scaffold a deployable agent directory (agent.toml + prompt.md + skills/)", + Short: "Scaffold a deployable agent directory (prompt agent, or a langgraph project)", Example: " oc agent init\n" + " oc agent init ./agents/triage --name triage --model anthropic/claude-sonnet-5\n" + - " oc agent init && $EDITOR prompt.md && oc agent deploy", + " oc agent init ./agents/grapher --runtime langgraph", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { dir := "." @@ -59,33 +232,28 @@ var agentInitCmd = &cobra.Command{ } } - if err := os.MkdirAll(filepath.Join(dir, "skills", "example"), 0o755); err != nil { - return err - } - files := []struct{ path, content string }{ - {filepath.Join(dir, "agent.toml"), fmt.Sprintf(agentTomlTmpl, name, model, runtime)}, - {filepath.Join(dir, "prompt.md"), promptTmpl}, - {filepath.Join(dir, "skills", "example", "SKILL.md"), skillTmpl}, - } - created := 0 - for _, f := range files { - if _, err := os.Stat(f.path); err == nil { - fmt.Printf(" skip %s (exists)\n", f.path) - continue - } - if err := os.WriteFile(f.path, []byte(f.content), 0o644); err != nil { + switch runtime { + case "langgraph": + return scaffoldLangGraph(dir, name, model) + case "flue": + return fmt.Errorf("flue scaffolding not wired yet; see sdks/flue oc-flue-starter (langgraph is: oc agent init --runtime langgraph)") + default: + // Prompt-based runtimes (claude|codex|pi). + if err := os.MkdirAll(filepath.Join(dir, "skills", "example"), 0o755); err != nil { return err } - fmt.Printf(" create %s\n", f.path) - created++ + files := []struct{ path, content string }{ + {filepath.Join(dir, "agent.toml"), fmt.Sprintf(agentTomlTmpl, name, model, runtime)}, + {filepath.Join(dir, "prompt.md"), promptTmpl}, + {filepath.Join(dir, "skills", "example", "SKILL.md"), skillTmpl}, + } + return writeScaffold(files, dir, "Edit prompt.md, then: oc agent deploy "+dir) } - fmt.Printf("\nScaffolded %d file(s). Edit prompt.md, then: oc agent deploy %s\n", created, dir) - return nil }, } func init() { agentInitCmd.Flags().String("name", "", "Agent name (default: the directory name)") agentInitCmd.Flags().String("model", "anthropic/claude-sonnet-5", "Model") - agentInitCmd.Flags().String("runtime", "claude", "Runtime family (claude|codex|pi)") + agentInitCmd.Flags().String("runtime", "claude", "Runtime family (claude|codex|pi|flue|langgraph)") } diff --git a/cmd/oc/internal/langgraphbuild/build_test.go b/cmd/oc/internal/langgraphbuild/build_test.go new file mode 100644 index 00000000..2ea5fd13 --- /dev/null +++ b/cmd/oc/internal/langgraphbuild/build_test.go @@ -0,0 +1,38 @@ +package langgraphbuild + +import ( + "context" + "os" + "strings" + "testing" +) + +// TestBuild_Fixture runs the real wrangler bundle against an installed langgraph +// scaffold and asserts the artifact shape. Gated on LANGGRAPH_BUILD_FIXTURE (an +// `oc agent init --runtime langgraph` dir with `npm install` already run) because it +// needs node_modules + wrangler present. +func TestBuild_Fixture(t *testing.T) { + dir := os.Getenv("LANGGRAPH_BUILD_FIXTURE") + if dir == "" { + t.Skip("set LANGGRAPH_BUILD_FIXTURE to an installed scaffold dir") + } + res, err := Build(context.Background(), dir) + if err != nil { + t.Fatalf("Build: %v", err) + } + if !strings.HasPrefix(res.Digest, "sha256:") || len(res.Digest) != len("sha256:")+64 { + t.Fatalf("digest = %q (want sha256:<64 hex>)", res.Digest) + } + if len(res.Bundle) == 0 || res.SizeBytes != int64(len(res.Bundle)) { + t.Fatalf("bundle %d bytes, size=%d (want non-empty + matching)", len(res.Bundle), res.SizeBytes) + } + if res.Wrangler.Main != "app.js" || !res.Wrangler.NoBundle { + t.Fatalf("descriptor main=%q noBundle=%v (want app.js / true)", res.Wrangler.Main, res.Wrangler.NoBundle) + } + if len(res.Wrangler.DurableObjects.Bindings) == 0 || res.Entrypoint == "" { + t.Fatalf("missing DO binding / entrypoint: %+v", res.Wrangler.DurableObjects) + } + t.Logf("ok: %s… %d bytes gzip, main=%s, DO %s->%s, compat=%s", + res.Digest[:23], res.SizeBytes, res.Wrangler.Main, + res.Wrangler.DurableObjects.Bindings[0].Name, res.Entrypoint, res.Wrangler.CompatibilityDate) +} diff --git a/cmd/oc/internal/langgraphbuild/langgraphbuild.go b/cmd/oc/internal/langgraphbuild/langgraphbuild.go new file mode 100644 index 00000000..f9408075 --- /dev/null +++ b/cmd/oc/internal/langgraphbuild/langgraphbuild.go @@ -0,0 +1,196 @@ +// Package langgraphbuild produces a deployable Worker-for-Platforms artifact from a +// LangGraph.js agent project (its wrangler.jsonc + src/app.ts). It is intentionally +// independent of the flue build path (the langgraph runtime is its own thing) but +// emits the SAME artifact shape — a tar.gz of the pre-bundled module + a strict +// wrangler descriptor + a sha256 digest — so the platform hosts a langgraph Worker +// identically to a flue one (model A: the Worker self-hosts its session transport). +package langgraphbuild + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// DOBinding is one Durable Object binding (the session store). +type DOBinding struct { + Name string `json:"name"` + ClassName string `json:"class_name"` +} + +// WranglerDescriptor is the strict Worker-for-Platforms subset the platform accepts; +// raw wrangler config never crosses the boundary. NoBundle is always true here — the +// project's own wrangler already produced the final module. +type WranglerDescriptor struct { + Main string `json:"main"` + CompatibilityDate string `json:"compatibility_date"` + CompatibilityFlags []string `json:"compatibility_flags"` + NoBundle bool `json:"no_bundle"` + DurableObjects struct { + Bindings []DOBinding `json:"bindings"` + } `json:"durable_objects"` +} + +// Result is the built artifact: the tar.gz bundle bytes plus its content-addressed +// digest and the descriptor referencing it. +type Result struct { + Wrangler WranglerDescriptor + Bundle []byte + Digest string + SizeBytes int64 + Entrypoint string // the session Durable Object class the platform routes to +} + +type wranglerConfig struct { + Name string `json:"name"` + Main string `json:"main"` + CompatibilityDate string `json:"compatibility_date"` + CompatibilityFlags []string `json:"compatibility_flags"` + DurableObjects struct { + Bindings []DOBinding `json:"bindings"` + } `json:"durable_objects"` +} + +// Build bundles the project at dir into a Result. It shells out to the project's own +// wrangler (`deploy --dry-run --outdir`) so the bundle honors wrangler.jsonc exactly +// (DO bindings, compatibility flags, nodejs_compat), then tars the emitted module. +func Build(ctx context.Context, dir string) (Result, error) { + raw, err := os.ReadFile(filepath.Join(dir, "wrangler.jsonc")) + if err != nil { + return Result{}, fmt.Errorf("read wrangler.jsonc: %w", err) + } + var cfg wranglerConfig + if err := json.Unmarshal(stripJSONC(raw), &cfg); err != nil { + return Result{}, fmt.Errorf("parse wrangler.jsonc: %w", err) + } + if cfg.Main == "" { + return Result{}, fmt.Errorf("wrangler.jsonc: missing \"main\"") + } + if len(cfg.DurableObjects.Bindings) == 0 { + return Result{}, fmt.Errorf("wrangler.jsonc: a langgraph agent needs a Durable Object binding (its session store)") + } + + outDir, err := os.MkdirTemp("", "oc-langgraph-build-*") + if err != nil { + return Result{}, err + } + defer os.RemoveAll(outDir) + + bin := wranglerBin(dir) + args := append(bin[1:], "deploy", "--dry-run", "--outdir", outDir) + c := exec.CommandContext(ctx, bin[0], args...) + c.Dir = dir + c.Env = append(os.Environ(), "WRANGLER_SEND_METRICS=false") + var stderr bytes.Buffer + c.Stderr = &stderr + if err := c.Run(); err != nil { + return Result{}, fmt.Errorf("wrangler bundle failed: %w\n%s", err, strings.TrimSpace(stderr.String())) + } + + // src/app.ts -> app.js + mainName := strings.TrimSuffix(filepath.Base(cfg.Main), filepath.Ext(cfg.Main)) + ".js" + if _, err := os.Stat(filepath.Join(outDir, mainName)); err != nil { + return Result{}, fmt.Errorf("bundled main %q not found in wrangler output: %w", mainName, err) + } + + bundle, err := tarGzModules(outDir) + if err != nil { + return Result{}, err + } + sum := sha256.Sum256(bundle) + + desc := WranglerDescriptor{ + Main: mainName, + CompatibilityDate: cfg.CompatibilityDate, + CompatibilityFlags: cfg.CompatibilityFlags, + NoBundle: true, + } + desc.DurableObjects.Bindings = cfg.DurableObjects.Bindings + + return Result{ + Wrangler: desc, + Bundle: bundle, + Digest: "sha256:" + hex.EncodeToString(sum[:]), + SizeBytes: int64(len(bundle)), + Entrypoint: cfg.DurableObjects.Bindings[0].ClassName, + }, nil +} + +// wranglerBin prefers the project-local wrangler, falling back to npx. +func wranglerBin(dir string) []string { + local := filepath.Join(dir, "node_modules", ".bin", "wrangler") + if _, err := os.Stat(local); err == nil { + return []string{local} + } + return []string{"npx", "--yes", "wrangler"} +} + +// tarGzModules tars the emitted *.js module(s) (not sourcemaps) into a gzip archive. +func tarGzModules(dir string) ([]byte, error) { + entries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + var buf bytes.Buffer + gz := gzip.NewWriter(&buf) + tw := tar.NewWriter(gz) + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(e.Name(), ".js") { + continue // skip .js.map, README, etc. + } + data, err := os.ReadFile(filepath.Join(dir, e.Name())) + if err != nil { + return nil, err + } + if err := tw.WriteHeader(&tar.Header{Name: e.Name(), Mode: 0o644, Size: int64(len(data))}); err != nil { + return nil, err + } + if _, err := tw.Write(data); err != nil { + return nil, err + } + } + if err := tw.Close(); err != nil { + return nil, err + } + if err := gz.Close(); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// stripJSONC removes // line and /* */ block comments so a wrangler.jsonc parses as +// JSON. Simplistic (ignores comment-like sequences inside strings) — fine for the +// generated descriptor; hand-edits with such strings should use plain JSON. +func stripJSONC(b []byte) []byte { + s := string(b) + for { + i := strings.Index(s, "/*") + if i < 0 { + break + } + j := strings.Index(s[i:], "*/") + if j < 0 { + s = s[:i] + break + } + s = s[:i] + s[i+j+2:] + } + var out strings.Builder + for _, line := range strings.Split(s, "\n") { + if k := strings.Index(line, "//"); k >= 0 { + line = line[:k] + } + out.WriteString(line) + out.WriteByte('\n') + } + return []byte(out.String()) +} diff --git a/sdks/langgraph/.gitignore b/sdks/langgraph/.gitignore new file mode 100644 index 00000000..dd6e803c --- /dev/null +++ b/sdks/langgraph/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.log +.DS_Store diff --git a/sdks/langgraph/README.md b/sdks/langgraph/README.md new file mode 100644 index 00000000..ae1c7264 --- /dev/null +++ b/sdks/langgraph/README.md @@ -0,0 +1,54 @@ +# @opencomputer/langgraph + +Run a [LangGraph.js](https://langchain-ai.github.io/langgraphjs/) graph as an +OpenComputer agent. A **standalone runtime** — its own session transport, one Durable +Object per session, a durable checkpointer, and model-gateway wiring — with **no +`@flue/runtime` dependency**. Scaffold one with: + +``` +oc agent init ./my-graph --runtime langgraph +``` + +## What it gives you + +- **`createLangGraphRuntime({ compile })`** → `{ fetch, SessionDO }`. Mount both in your + `src/app.ts`: `fetch` is the Worker host (its transport: `/health`, and + `POST|GET /agents/:agent/:session`), `SessionDO` is the per-session Durable Object that + runs your graph. `compile(checkpointer)` is your `StateGraph.compile({ checkpointer })` + — the runtime passes in a durable checkpointer per session. +- **`DurableObjectSaver`** — a LangGraph `BaseCheckpointSaver` on Durable Object storage, + so a thread's state survives isolate eviction and resumes across invocations. + (Postgres/Redis savers can't run on the Workers runtime — long-lived TCP.) +- **`ocModel(env, opts?)`** — a LangChain Anthropic model pointed at the OC gateway when + deployed (`OC_GATEWAY` + `OC_SESSION_TOKEN`) or `ANTHROPIC_API_KEY` locally. Build it + **inside a node from the request env** (`config.configurable.env`), not at module init + — the token is a per-request secret. + +```ts +// src/app.ts +import { createLangGraphRuntime } from "@opencomputer/langgraph"; +import { compile } from "./graph.js"; +const runtime = createLangGraphRuntime({ compile }); +export default { fetch: runtime.fetch }; +export const LangGraphSession = runtime.SessionDO; // matches wrangler.jsonc class_name +``` + +## Transport contract + +The server-side langgraph dispatch targets this: + +- `GET /health` → `{ status: "ok" }` +- `POST /agents/:agent/:session` body `{ input? , messages? }` → runs the graph for that + session's thread, returns `{ session, status, state, events }` +- `GET /agents/:agent/:session?offset=N` → `{ session, offset, events }` (replay) + +Each `(agent, session)` maps to one Durable Object; the graph is compiled there with a +`DurableObjectSaver`. + +## Status + +Runtime (host + Session DO + `DurableObjectSaver` + `ocModel`) is built and typechecks. +**Remaining for a live deploy:** (1) a `wrangler`-based build step wired into +`oc agent deploy`'s langgraph branch; (2) the server-side dispatch registering the +`langgraph` family and routing sessions to the deployed Worker; (3) an integration test +(miniflare) exercising the checkpointer + transport end to end. diff --git a/sdks/langgraph/package-lock.json b/sdks/langgraph/package-lock.json new file mode 100644 index 00000000..e364fae7 --- /dev/null +++ b/sdks/langgraph/package-lock.json @@ -0,0 +1,2076 @@ +{ + "name": "@opencomputer/langgraph", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@opencomputer/langgraph", + "version": "0.0.1", + "license": "Apache-2.0", + "devDependencies": { + "@langchain/anthropic": "^1.5.1", + "@langchain/core": "^1.2.3", + "@langchain/langgraph": "^1.4.8", + "@types/node": "^22.10.0", + "typescript": "^5.9.3", + "vitest": "^3.0.0" + }, + "engines": { + "node": ">=22.19" + }, + "peerDependencies": { + "@langchain/anthropic": ">=1.5.0", + "@langchain/core": ">=1.2.0", + "@langchain/langgraph": ">=1.4.0" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.103.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.103.0.tgz", + "integrity": "sha512-1uG7RNgoHTUxzOXqSCODKt0UTVlxWiHk/2Tt2/uQJiPW7XzBeKVuJyd3Aw6T3LPyvZV/jDTnPLX7SaM70WLLjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cfworker/json-schema": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", + "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@langchain/anthropic": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.5.1.tgz", + "integrity": "sha512-j92zCCd5BFH3rHMRzc2wBmSKDoVpinof1oh8aFiAz9TWbSOc4tGU4n6bqwy/wP0GH1uO96zZHLGCHBMPgrxTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "^0.103.0", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@langchain/core": "^1.2.1" + } + }, + "node_modules/@langchain/core": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.2.3.tgz", + "integrity": "sha512-F+L5SsciykwDl7eDxacnhDTcWe1IF6jetzfkvI5PPfq6ogWHO7xcjU90SGh/3lqbbS0tgun+qF01KIqxawrCsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cfworker/json-schema": "^4.0.2", + "@standard-schema/spec": "^1.1.0", + "js-tiktoken": "^1.0.12", + "langsmith": ">=0.5.0 <1.0.0", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@langchain/langgraph": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.4.8.tgz", + "integrity": "sha512-DN1Np1XefdBEbp1qBKlt39cwoL743AAGpR5Ipja0gY2YbWvsoQnOTIrjnj/orSAhaUYsdTKS8VSWdFzsHZo6Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@langchain/langgraph-checkpoint": "^1.1.3", + "@langchain/langgraph-sdk": "~1.9.26", + "@langchain/protocol": "^0.0.18", + "@standard-schema/spec": "1.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": "^1.1.48", + "zod": "^3.25.32 || ^4.2.0" + } + }, + "node_modules/@langchain/langgraph-checkpoint": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.1.3.tgz", + "integrity": "sha512-wgzdQNeEsdw1e+4lvlj0tdq/RYR/k1vPin10g0ymGoehZDDgd9nvIllGXSXN4TFgF9sf5qQP/KTkOcLfeseIhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": "^1.1.48" + } + }, + "node_modules/@langchain/langgraph-sdk": { + "version": "1.9.28", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.9.28.tgz", + "integrity": "sha512-4j3XuM0PvtmAbL8mPfBS99ez3+ytRfgbOpAR/nOeaejTRF3Q9dNw2QnaGLGng8wLPtGLoSj+SYgUOVxy9Bv9vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@langchain/protocol": "^0.0.18", + "@types/json-schema": "^7.0.15", + "p-queue": "^9.0.1", + "p-retry": "^7.1.1" + }, + "peerDependencies": { + "@langchain/core": "^1.1.48", + "react": "^18 || ^19", + "react-dom": "^18 || ^19", + "svelte": "^4.0.0 || ^5.0.0", + "vue": "^3.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@langchain/langgraph-sdk/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@langchain/langgraph-sdk/node_modules/p-queue": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.3.tgz", + "integrity": "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@langchain/langgraph-sdk/node_modules/p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@langchain/protocol": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@langchain/protocol/-/protocol-0.0.18.tgz", + "integrity": "sha512-XW1egQtPfsGI41w2AMZNFZrUIwFSQHTjVMZs0OaTpCAvht/QLoaPN8FQcsysMVypOhupG28J29yOorrc70otBQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/is-network-error": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", + "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tiktoken": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", + "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==", + "dev": true, + "license": "MIT", + "dependencies": { + "base64-js": "^1.5.1" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/langsmith": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.8.4.tgz", + "integrity": "sha512-e2zdhPUV/mLwVv+Gde/0gLGnCOE/zvZ3gGNh/rvkzrxsDz7qgUT4CJVUmJE2GwQ1A3FPtWKzy08oo//VV1nBFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-queue": "6.6.2" + }, + "peerDependencies": { + "@opentelemetry/api": "*", + "@opentelemetry/exporter-trace-otlp-proto": "*", + "@opentelemetry/sdk-trace-base": "*", + "openai": "*", + "ws": ">=7" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-proto": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + }, + "openai": { + "optional": true + }, + "ws": { + "optional": true + } + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz", + "integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-network-error": "^1.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/sdks/langgraph/package.json b/sdks/langgraph/package.json new file mode 100644 index 00000000..9f1ecd59 --- /dev/null +++ b/sdks/langgraph/package.json @@ -0,0 +1,56 @@ +{ + "name": "@opencomputer/langgraph", + "version": "0.0.1", + "description": "Run a LangGraph.js graph as an OpenComputer agent — managed model gateway + durable checkpointing.", + "keywords": [ + "agents", + "langgraph", + "langchain", + "opencomputer", + "cloudflare-workers", + "durable-objects" + ], + "author": "OpenComputer", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/diggerhq/opencomputer.git", + "directory": "sdks/langgraph" + }, + "type": "module", + "engines": { + "node": ">=22.19" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"", + "build": "npm run clean && tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "vitest run" + }, + "peerDependencies": { + "@langchain/anthropic": ">=1.5.0", + "@langchain/core": ">=1.2.0", + "@langchain/langgraph": ">=1.4.0" + }, + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@langchain/anthropic": "^1.5.1", + "@langchain/core": "^1.2.3", + "@langchain/langgraph": "^1.4.8", + "@types/node": "^22.10.0", + "typescript": "^5.9.3", + "vitest": "^3.0.0" + } +} diff --git a/sdks/langgraph/src/checkpointer.test.ts b/sdks/langgraph/src/checkpointer.test.ts new file mode 100644 index 00000000..677f506a --- /dev/null +++ b/sdks/langgraph/src/checkpointer.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect } from "vitest"; +import { StateGraph, MessagesAnnotation, START, END } from "@langchain/langgraph"; +import { AIMessage, HumanMessage } from "@langchain/core/messages"; +import { DurableObjectSaver, type DOStorage } from "./checkpointer.js"; + +// A Map-backed DOStorage — the same async KV surface Cloudflare Durable Object +// storage exposes, so this exercises the saver's real serialization + key logic +// without needing the Workers runtime. +function memStorage(): DOStorage { + const m = new Map(); + return { + async get(k: string) { return m.get(k) as T | undefined; }, + async put(k, v) { m.set(k, v); }, + async delete(k) { return m.delete(k); }, + async list(opts?: { prefix?: string; reverse?: boolean; limit?: number }) { + let keys = [...m.keys()].filter((k) => !opts?.prefix || k.startsWith(opts.prefix)).sort(); + if (opts?.reverse) keys.reverse(); + if (opts?.limit != null) keys = keys.slice(0, opts.limit); + const out = new Map(); + for (const k of keys) out.set(k, m.get(k) as T); + return out; + }, + }; +} + +const appendNode = new StateGraph(MessagesAnnotation) + .addNode("append", async (s) => ({ messages: [new AIMessage("turn:" + s.messages.length)] })) + .addEdge(START, "append") + .addEdge("append", END); + +describe("DurableObjectSaver", () => { + it("persists a thread and resumes it across a fresh saver instance", async () => { + const storage = memStorage(); + const cfg = { configurable: { thread_id: "t1" } }; + + // Turn 1: input [human] -> node appends one message -> 2 messages. + const g1 = appendNode.compile({ checkpointer: new DurableObjectSaver(storage) }); + const r1 = await g1.invoke({ messages: [new HumanMessage("hi")] }, cfg); + expect(r1.messages.length).toBe(2); + + // Turn 2 with a BRAND-NEW saver over the SAME storage — if state came only from + // memory this would be 2; resuming from durable storage makes it 4 + // (prior 2 + new human + new appended turn). + const g2 = appendNode.compile({ checkpointer: new DurableObjectSaver(storage) }); + const r2 = await g2.invoke({ messages: [new HumanMessage("again")] }, cfg); + expect(r2.messages.length).toBe(4); + + // getTuple (no checkpoint_id) returns the latest committed checkpoint. + const latest = await new DurableObjectSaver(storage).getTuple(cfg); + expect(latest?.checkpoint.id).toBeTruthy(); + + // list walks the thread's checkpoint history (multiple across the two turns). + const seen: string[] = []; + for await (const t of new DurableObjectSaver(storage).list(cfg)) seen.push(t.checkpoint.id); + expect(seen.length).toBeGreaterThan(1); + }); + + it("isolates threads and deletes them", async () => { + const storage = memStorage(); + const saver = new DurableObjectSaver(storage); + const g = appendNode.compile({ checkpointer: saver }); + await g.invoke({ messages: [new HumanMessage("a")] }, { configurable: { thread_id: "A" } }); + await g.invoke({ messages: [new HumanMessage("b")] }, { configurable: { thread_id: "B" } }); + + expect(await saver.getTuple({ configurable: { thread_id: "A" } })).toBeDefined(); + await saver.deleteThread("A"); + expect(await saver.getTuple({ configurable: { thread_id: "A" } })).toBeUndefined(); + // B is untouched. + expect(await saver.getTuple({ configurable: { thread_id: "B" } })).toBeDefined(); + }); +}); diff --git a/sdks/langgraph/src/checkpointer.ts b/sdks/langgraph/src/checkpointer.ts new file mode 100644 index 00000000..c0cdbc0e --- /dev/null +++ b/sdks/langgraph/src/checkpointer.ts @@ -0,0 +1,119 @@ +// A LangGraph BaseCheckpointSaver backed by Cloudflare Durable Object storage — +// the durable memory for a langgraph agent's session. Modeled on the built-in +// MemorySaver's layout ([threadId, checkpoint_ns, checkpoint_id] keys, writes +// indexed by `${taskId},${idx}`, values run through `serde`) but persisted to the +// DO's async KV so runs survive isolate eviction / hibernation. Postgres/Redis +// savers can't run on Workers (long-lived TCP); this is the Workers-native one. + +import { + BaseCheckpointSaver, + type Checkpoint, + type CheckpointListOptions, + type CheckpointMetadata, + type CheckpointTuple, + type ChannelVersions, + type PendingWrite, + type SerializerProtocol, +} from "@langchain/langgraph-checkpoint"; +import type { RunnableConfig } from "@langchain/core/runnables"; + +/** Minimal async KV surface — Cloudflare Durable Object `state.storage` implements it. */ +export interface DOStorage { + get(key: string): Promise; + put(key: string, value: unknown): Promise; + delete(key: string): Promise; + list(options?: { prefix?: string; reverse?: boolean; limit?: number }): Promise>; +} + +type SerdeVal = { t: string; d: unknown }; +type StoredCheckpoint = { checkpoint: SerdeVal; metadata: SerdeVal; parent?: string }; +type StoredWrites = Record; + +const ns = (v?: string | null) => v ?? ""; + +export class DurableObjectSaver extends BaseCheckpointSaver { + constructor(private storage: DOStorage, serde?: SerializerProtocol) { + super(serde); + } + + private ckptKey(t: string, n: string, id: string) { return `ckpt:${t}:${n}:${id}`; } + private latestKey(t: string, n: string) { return `latest:${t}:${n}`; } + private writesKey(t: string, n: string, id: string) { return `writes:${t}:${n}:${id}`; } + + private async dump(x: unknown): Promise { const [t, d] = await this.serde.dumpsTyped(x); return { t, d }; } + private async load(v: SerdeVal): Promise { + return (await this.serde.loadsTyped(v.t, v.d as Uint8Array)) as T; + } + + async getTuple(config: RunnableConfig): Promise { + const t = config.configurable?.thread_id as string | undefined; + if (t === undefined) return undefined; + const n = ns(config.configurable?.checkpoint_ns); + let id = config.configurable?.checkpoint_id as string | undefined; + if (!id) id = await this.storage.get(this.latestKey(t, n)); + if (!id) return undefined; + const saved = await this.storage.get(this.ckptKey(t, n, id)); + if (!saved) return undefined; + const writesMap = (await this.storage.get(this.writesKey(t, n, id))) ?? {}; + const pendingWrites: [string, string, unknown][] = []; + for (const [taskId, channel, value] of Object.values(writesMap)) { + pendingWrites.push([taskId, channel, await this.load(value)]); + } + const tuple: CheckpointTuple = { + config: { configurable: { thread_id: t, checkpoint_ns: n, checkpoint_id: id } }, + checkpoint: await this.load(saved.checkpoint), + metadata: await this.load(saved.metadata), + pendingWrites, + }; + if (saved.parent) { + tuple.parentConfig = { configurable: { thread_id: t, checkpoint_ns: n, checkpoint_id: saved.parent } }; + } + return tuple; + } + + async *list(config: RunnableConfig, options?: CheckpointListOptions): AsyncGenerator { + const t = config.configurable?.thread_id as string | undefined; + if (t === undefined) return; + const n = ns(config.configurable?.checkpoint_ns); + const prefix = `ckpt:${t}:${n}:`; + const map = await this.storage.list({ prefix, reverse: true, limit: options?.limit }); + let yielded = 0; + for (const key of map.keys()) { + const id = key.slice(prefix.length); + const tuple = await this.getTuple({ configurable: { thread_id: t, checkpoint_ns: n, checkpoint_id: id } }); + if (tuple) { yield tuple; yielded++; } + if (options?.limit && yielded >= options.limit) return; + } + } + + async put(config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata, _newVersions: ChannelVersions): Promise { + const t = config.configurable?.thread_id as string; + const n = ns(config.configurable?.checkpoint_ns); + const id = checkpoint.id; + const parent = config.configurable?.checkpoint_id as string | undefined; + const stored: StoredCheckpoint = { checkpoint: await this.dump(checkpoint), metadata: await this.dump(metadata), parent }; + await this.storage.put(this.ckptKey(t, n, id), stored); + await this.storage.put(this.latestKey(t, n), id); + return { configurable: { thread_id: t, checkpoint_ns: n, checkpoint_id: id } }; + } + + async putWrites(config: RunnableConfig, writes: PendingWrite[], taskId: string): Promise { + const t = config.configurable?.thread_id as string; + const n = ns(config.configurable?.checkpoint_ns); + const id = config.configurable?.checkpoint_id as string; + const key = this.writesKey(t, n, id); + const existing = (await this.storage.get(key)) ?? {}; + for (let idx = 0; idx < writes.length; idx++) { + const [channel, value] = writes[idx]; + existing[`${taskId},${idx}`] = [taskId, channel, await this.dump(value)]; + } + await this.storage.put(key, existing); + } + + async deleteThread(threadId: string): Promise { + for (const prefix of [`ckpt:${threadId}:`, `writes:${threadId}:`, `latest:${threadId}:`]) { + const map = await this.storage.list({ prefix }); + for (const key of map.keys()) await this.storage.delete(key); + } + } +} diff --git a/sdks/langgraph/src/gateway.ts b/sdks/langgraph/src/gateway.ts new file mode 100644 index 00000000..9e956e71 --- /dev/null +++ b/sdks/langgraph/src/gateway.ts @@ -0,0 +1,49 @@ +// OC model-gateway wiring for LangGraph.js agents (parallels @opencomputer/flue's +// useOcGateway). A LangGraph graph calls models through LangChain chat models; this +// points one at the OC gateway — a thin Worker over the providers that injects the +// org key + meters per session. +// +// TOKEN SEAM (same as flue's gateway.ts): OC_GATEWAY is a plain var, but +// OC_SESSION_TOKEN is a per-deploy Worker SECRET only readable on the per-REQUEST +// env, not at module/init scope. So construct the model INSIDE the node/turn from +// the request env — never cache a model built at init, or the apiKey reads empty and +// every call throws "no API key". `ocModel(env)` takes the env explicitly for that +// reason; on Node/local it defaults to process.env and ANTHROPIC_API_KEY. + +import { ChatAnthropic } from "@langchain/anthropic"; + +export interface OcGatewayEnv { + /** Deployed gateway Worker base URL (injected per deploy). */ + OC_GATEWAY?: string; + /** Signed per-deploy JWT the gateway verifies (never a raw provider key). */ + OC_SESSION_TOKEN?: string; + /** Local fallback when OC_GATEWAY is unset. */ + ANTHROPIC_API_KEY?: string; + [key: string]: unknown; +} + +export interface OcModelOptions { + /** Anthropic model id (dashed pi-ai catalog id, e.g. claude-sonnet-4-6). */ + model?: string; + /** Extra ChatAnthropic options (temperature, maxTokens, …). */ + overrides?: Record; +} + +/** + * Build a LangChain Anthropic model pointed at the OC gateway when deployed + * (OC_GATEWAY + OC_SESSION_TOKEN), or at ANTHROPIC_API_KEY locally. Call this + * per-request/per-node (see token-seam note) — do not cache the result at init. + */ +export function ocModel(env: OcGatewayEnv, opts: OcModelOptions = {}): ChatAnthropic { + const model = opts.model ?? "claude-sonnet-4-6"; + const gw = env.OC_GATEWAY; + if (gw) { + return new ChatAnthropic({ + model, + apiKey: env.OC_SESSION_TOKEN, + anthropicApiUrl: gw.replace(/\/+$/, "") + "/anthropic", + ...opts.overrides, + }); + } + return new ChatAnthropic({ model, apiKey: env.ANTHROPIC_API_KEY, ...opts.overrides }); +} diff --git a/sdks/langgraph/src/index.ts b/sdks/langgraph/src/index.ts new file mode 100644 index 00000000..586a9b63 --- /dev/null +++ b/sdks/langgraph/src/index.ts @@ -0,0 +1,23 @@ +// @opencomputer/langgraph — run a LangGraph.js graph as an OpenComputer agent. +// A standalone runtime (no @flue/runtime dependency): its own session transport, +// per-session Durable Object, durable checkpointer, and model-gateway wiring. +// +// - createLangGraphRuntime({ compile }) -> { fetch, SessionDO }: the Worker host + +// the per-session Durable Object that runs your graph. Wire both in your app.ts. +// - DurableObjectSaver: a LangGraph BaseCheckpointSaver on Durable Object storage, +// so a thread's state survives isolate eviction and resumes across invocations. +// - ocModel(env): a LangChain Anthropic model pointed at the OC model gateway +// (managed key + per-session metering), built per-request for the token seam. +export { createLangGraphRuntime } from "./runtime.js"; +export type { + CompiledGraph, + DurableObjectNamespace, + DurableObjectState, + LangGraphRuntime, + LangGraphRuntimeOptions, + SessionDurableObject, +} from "./runtime.js"; +export { DurableObjectSaver } from "./checkpointer.js"; +export type { DOStorage } from "./checkpointer.js"; +export { ocModel } from "./gateway.js"; +export type { OcGatewayEnv, OcModelOptions } from "./gateway.js"; diff --git a/sdks/langgraph/src/runtime.ts b/sdks/langgraph/src/runtime.ts new file mode 100644 index 00000000..b849e1ba --- /dev/null +++ b/sdks/langgraph/src/runtime.ts @@ -0,0 +1,333 @@ +// Standalone OpenComputer hosting for a LangGraph.js graph, as a Workers-for-Platforms +// tenant: one Durable Object per session that speaks the OC agent-Worker contract +// (oc-runtimes/agent-worker-hosting) so it rides flue's deploy + dispatch + tailer plane. +// Zero @flue/runtime dependency — the graph runs in the DO with a DO-backed checkpointer. +// +// The dispatch Worker strips `/dispatch/` and forwards BYTE-EXACT, so the tenant +// Worker sees exactly: +// GET /health -> 200 (deploy settle probe) +// POST /agents// {"message"} -> 202 {submissionId, offset} (admit) +// GET /agents//?view=updates&offset=&live=long-poll +// -> 200 ConversationStreamChunk[] (tail) +// headers Stream-Next-Offset, Stream-Up-To-Date +// -> 499 when idle (long-poll expired, up to date) +// POST /agents///abort -> 202 (cancel the running turn) +// +// A turn runs from a durable DO ALARM (survives isolate eviction): admit records the +// pending message + arms the alarm and returns the receipt immediately; the alarm streams +// the graph, appending ConversationStreamChunks to an offset-addressed durable log that the +// OC tailer drains via ?view=updates. The chunk stream is derived generically from LangGraph +// `streamEvents(v2)`, so it works for ANY user graph (single node, ReAct, multi-node). + +import { DurableObjectSaver, type DOStorage } from "./checkpointer.js"; +import type { BaseCheckpointSaver } from "@langchain/langgraph-checkpoint"; + +// ── ConversationStreamChunk (contract #2; mirrors sessions-api core/flue-tailer.ts) ── +// The tenant emits ASSEMBLY chunks; the OC tailer reduces them into Tier-1 OC events. +export type StreamChunk = + | { type: "message-started"; messageId: string; submissionId?: string; model?: { provider: string; id: string }; turnId?: string } + | { type: "message-delta"; messageId: string; kind: "text" | "reasoning"; delta: string } + | { type: "tool-input"; messageId: string; toolCallId: string; toolName: string; input: unknown } + | { type: "tool-output"; toolCallId: string; output: unknown; durationMs?: number } + | { type: "tool-output-error"; toolCallId: string; errorText: string; durationMs?: number } + | { type: "message-completed"; messageId: string; usage?: unknown } + | { type: "submission-settled"; submissionId: string; outcome: "completed" | "failed" | "aborted"; error?: unknown }; + +// ── Minimal Cloudflare Durable Object surface (avoids a @cloudflare/workers-types dep) ── +export interface DOStorageWithAlarm extends DOStorage { + setAlarm(scheduledTime: number): Promise; + getAlarm(): Promise; +} +export interface DurableObjectState { + storage: DOStorageWithAlarm; + blockConcurrencyWhile(fn: () => Promise): Promise; +} +export interface DurableObjectId { toString(): string } +export interface DurableObjectStub { fetch(req: Request): Promise } +export interface DurableObjectNamespace { + idFromName(name: string): DurableObjectId; + get(id: DurableObjectId): DurableObjectStub; +} + +/** A compiled LangGraph graph — structurally what `StateGraph.compile()` returns (the bits we use). + * We drive off `stream(streamMode:"updates")` — the graph's own per-node output channel — NOT + * `streamEvents`: LangChain's callback context (AsyncLocalStorage) does not propagate to the model + * call in the Workers runtime, so streamEvents silently drops all on_chat_model_* events. */ +export interface CompiledGraph { + stream(input: unknown, options: Record): Promise>>; +} + +export interface LangGraphRuntimeOptions { + /** Compile the graph with the runtime-provided durable checkpointer. */ + compile: (checkpointer: BaseCheckpointSaver) => CompiledGraph; + /** Durable Object binding name in wrangler.jsonc (default "SESSION"). */ + binding?: string; +} + +type RuntimeEnv = Record; + +export interface SessionDurableObject { fetch(req: Request): Promise; alarm(): Promise } +export interface LangGraphRuntime { + fetch(req: Request, env: RuntimeEnv): Promise; + SessionDO: new (state: DurableObjectState, env: RuntimeEnv) => SessionDurableObject; +} + +const LONGPOLL_MS = 20_000; // how long a ?live=long-poll read waits for new chunks +const OFF = (n: number) => `chunk:${String(n).padStart(12, "0")}`; +const rid = (p: string) => `${p}_${Math.random().toString(16).slice(2, 14)}${Math.random().toString(16).slice(2, 6)}`; + +const json = (body: unknown, status = 200, headers: Record = {}): Response => + new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json", ...headers } }); + +interface Pending { message: string; submissionId: string } + +/** Pull the text + reasoning deltas out of a streamed AIMessageChunk's content. */ +function deltasOf(content: unknown): { text: string; reasoning: string } { + if (typeof content === "string") return { text: content, reasoning: "" }; + let text = "", reasoning = ""; + if (Array.isArray(content)) { + for (const block of content) { + if (!block || typeof block !== "object") continue; + const b = block as { type?: string; text?: string; thinking?: string; reasoning?: string }; + if (b.type === "text" && typeof b.text === "string") text += b.text; + else if ((b.type === "thinking" || b.type === "reasoning") && typeof (b.thinking ?? b.reasoning) === "string") { + reasoning += (b.thinking ?? b.reasoning) as string; + } + } + } + return { text, reasoning }; +} + +/** Map LangChain usage_metadata → the Flue PromptUsage shape the tailer's normalizer reads. */ +function usageOf(output: unknown): unknown { + const u = (output as { usage_metadata?: Record } | undefined)?.usage_metadata; + if (!u) return undefined; + const num = (v: unknown): number => (typeof v === "number" && Number.isFinite(v) ? v : 0); + const d = (u.input_token_details ?? {}) as Record; + return { + input: num(u.input_tokens), output: num(u.output_tokens), + cacheRead: num(d.cache_read), cacheWrite: num(d.cache_creation), + totalTokens: num(u.total_tokens), + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }; +} + +/** A LangChain message → OC ConversationStreamChunks. Graph-agnostic, whole-message granularity + * (streamMode "updates" gives completed messages, not tokens). `emitted` dedupes across updates. */ +function messageToChunks(m: unknown, submissionId: string, emitted: Set): StreamChunk[] { + const msg = m as { + id?: unknown; content?: unknown; tool_call_id?: unknown; usage_metadata?: unknown; + tool_calls?: Array<{ id?: string; name?: string; args?: unknown }>; + response_metadata?: Record; + _getType?: () => string; getType?: () => string; type?: string; role?: string; + }; + const kind = messageKind(msg); + if (kind === "tool") { + const toolCallId = typeof msg.tool_call_id === "string" ? msg.tool_call_id : rid("tool"); + return [{ type: "tool-output", toolCallId, output: msg.content }]; + } + if (kind !== "ai") return []; + const messageId = (typeof msg.id === "string" && msg.id) || rid("msg"); + if (emitted.has(messageId)) return []; + emitted.add(messageId); + const out: StreamChunk[] = []; + const model = modelOf(msg.response_metadata); + out.push({ type: "message-started", messageId, submissionId, ...(model ? { model } : {}) }); + const { text, reasoning } = deltasOf(msg.content); + if (reasoning) out.push({ type: "message-delta", messageId, kind: "reasoning", delta: reasoning }); + if (text) out.push({ type: "message-delta", messageId, kind: "text", delta: text }); + for (const tc of msg.tool_calls ?? []) { + out.push({ type: "tool-input", messageId, toolCallId: tc.id ?? rid("tool"), toolName: tc.name ?? "tool", input: tc.args }); + } + out.push({ type: "message-completed", messageId, usage: usageOf(msg) }); + return out; +} + +function messageKind(m: { _getType?: () => string; getType?: () => string; type?: string; role?: string }): "ai" | "tool" | "other" { + const t = (typeof m._getType === "function" ? m._getType() : undefined) + ?? (typeof m.getType === "function" ? m.getType() : undefined) + ?? m.type ?? m.role ?? ""; + if (t === "ai" || t === "assistant" || t === "AIMessage" || t === "AIMessageChunk") return "ai"; + if (t === "tool" || t === "ToolMessage") return "tool"; + return "other"; +} + +function modelOf(meta: Record | undefined): { provider: string; id: string } | undefined { + const id = str(meta?.model) || str(meta?.model_name); + return id ? { provider: "anthropic", id } : undefined; +} + +export function createLangGraphRuntime(opts: LangGraphRuntimeOptions): LangGraphRuntime { + const binding = opts.binding ?? "SESSION"; + + class SessionDO implements SessionDurableObject { + private graph: CompiledGraph; + private nextOffset = 0; + private ready: Promise; + private waiters: Array<() => void> = []; + private aborter: AbortController | null = null; + private session = "default"; + + constructor(private state: DurableObjectState, private env: RuntimeEnv) { + this.graph = opts.compile(new DurableObjectSaver(state.storage)); + this.ready = state.blockConcurrencyWhile(async () => { + this.nextOffset = (await state.storage.get("meta:nextOffset")) ?? 0; + }); + } + + async fetch(req: Request): Promise { + await this.ready; + const url = new URL(req.url); + const m = url.pathname.match(/^\/agents\/([^/]+)\/([^/]+?)(\/abort)?\/?$/); + if (!m) return json({ error: { type: "not_found" } }, 404); + this.session = decodeURIComponent(m[2]); + + if (req.method === "POST" && m[3] === "/abort") { + this.aborter?.abort(new Error("aborted by control plane")); + return json({ ok: true }, 202); + } + if (req.method === "POST") { + const body = (await req.json().catch(() => ({}))) as { message?: unknown }; + const message = typeof body.message === "string" ? body.message : ""; + return this.admit(message); + } + if (req.method === "GET" && url.searchParams.get("view") === "updates") { + const offset = Number.parseInt(url.searchParams.get("offset") ?? "-1", 10); + const longPoll = url.searchParams.get("live") === "long-poll"; + return this.tail(Number.isFinite(offset) ? offset : -1, longPoll); + } + return json({ error: { type: "not_found" } }, 404); + } + + /** Admit a turn: durably record it, arm the alarm, return the receipt immediately. */ + private async admit(message: string): Promise { + const submissionId = rid("sub"); + const pending: Pending = { message, submissionId }; + await this.state.storage.put("meta:pending", pending); + await this.state.storage.put("meta:activeSubmission", submissionId); + await this.state.storage.setAlarm(Date.now()); + // offset = last-written index (readers fetch index > offset), so this turn's first chunk + // is included. Fresh session (nextOffset 0) → "-1" = read from the very start. + return json({ submissionId, offset: String(this.nextOffset - 1) }, 202); + } + + /** Durable background turn runner — fires ~immediately after admit; survives eviction. */ + async alarm(): Promise { + await this.ready; + const pending = await this.state.storage.get("meta:pending"); + if (!pending) return; + await this.state.storage.delete("meta:pending"); + await this.runTurn(pending); + } + + private async runTurn(pending: Pending): Promise { + const { message, submissionId } = pending; + const aborter = new AbortController(); + this.aborter = aborter; + const emitted = new Set(); // message ids already turned into chunks (dedupe) + let timedOut = false; + + // Drain the graph's per-node output. streamMode "updates" yields + // { : }; for a MessagesAnnotation graph + // the delta is { messages: [...new messages] } → one or more OC chunks each. + const consume = async (): Promise => { + const stream = await this.graph.stream( + { messages: [{ role: "user", content: message }] }, + { streamMode: "updates", signal: aborter.signal, configurable: { thread_id: this.session, env: this.env } }, + ); + for await (const update of stream) { + if (aborter.signal.aborted) throw new Error("aborted"); + for (const nodeOut of Object.values(update ?? {})) { + const msgs = (nodeOut as { messages?: unknown } | null)?.messages; + if (!Array.isArray(msgs)) continue; + for (const m of msgs) { + for (const chunk of messageToChunks(m, submissionId, emitted)) await this.append(chunk); + } + } + } + }; + + // Backstop: a wedged node (a provider call that never settles, or a stream that neither + // rejects nor ends — observed with some Workers-runtime error paths) must not hang the + // session forever. Bound the turn, abort the graph, and settle it failed. + const budgetMs = Number(this.env.OC_TURN_BUDGET_MS) || 300_000; + let timer: ReturnType | undefined; + const watchdog = new Promise((_, reject) => { + timer = setTimeout(() => { + timedOut = true; + aborter.abort(new Error("turn timed out")); + reject(new Error(`turn exceeded ${budgetMs}ms`)); + }, budgetMs); + }); + + const consumeP = consume(); + void consumeP.catch(() => {}); // swallow a late rejection if the watchdog wins the race + try { + await Promise.race([consumeP, watchdog]); + await this.append({ type: "submission-settled", submissionId, outcome: "completed" }); + } catch (err) { + const aborted = aborter.signal.aborted && !timedOut; // control-plane /abort, not the watchdog + await this.append({ + type: "submission-settled", submissionId, + outcome: aborted ? "aborted" : "failed", + ...(aborted ? {} : { error: { message: err instanceof Error ? err.message : String(err) } }), + }); + } finally { + if (timer) clearTimeout(timer); + if (this.aborter === aborter) this.aborter = null; + await this.state.storage.delete("meta:activeSubmission"); + } + } + + /** Append one chunk to the durable offset log + wake any long-poll readers. */ + private async append(chunk: StreamChunk): Promise { + const idx = this.nextOffset; + await this.state.storage.put(OFF(idx), chunk); + this.nextOffset = idx + 1; + await this.state.storage.put("meta:nextOffset", this.nextOffset); + const wake = this.waiters; + this.waiters = []; + for (const w of wake) w(); + } + + private async readAfter(offset: number): Promise<{ chunks: StreamChunk[]; last: number }> { + const map = await this.state.storage.list({ prefix: "chunk:" }); + const items: Array<[number, StreamChunk]> = []; + for (const [key, chunk] of map) { + const i = Number.parseInt(key.slice("chunk:".length), 10); + if (i > offset) items.push([i, chunk]); + } + items.sort((a, b) => a[0] - b[0]); + return { chunks: items.map((x) => x[1]), last: items.length ? items[items.length - 1][0] : offset }; + } + + /** Serve ?view=updates: return chunks after `offset`, or long-poll then 499 when idle. */ + private async tail(offset: number, longPoll: boolean): Promise { + let { chunks, last } = await this.readAfter(offset); + if (chunks.length === 0 && longPoll) { + await new Promise((resolve) => { + const t = setTimeout(resolve, LONGPOLL_MS); + this.waiters.push(() => { clearTimeout(t); resolve(); }); + }); + ({ chunks, last } = await this.readAfter(offset)); + } + if (chunks.length === 0) return new Response(null, { status: 499 }); + return json(chunks, 200, { "Stream-Next-Offset": String(last), "Stream-Up-To-Date": "true" }); + } + } + + async function fetchHandler(req: Request, env: RuntimeEnv): Promise { + const url = new URL(req.url); + if (url.pathname === "/health" || url.pathname === "/healthz") return json({ status: "ok" }); + const m = url.pathname.match(/^\/agents\/([^/]+)\/([^/]+?)(?:\/abort)?\/?$/); + if (!m) return json({ error: { type: "not_found" } }, 404); + const ns = env[binding] as DurableObjectNamespace | undefined; + if (!ns) return json({ error: { type: "misconfigured", message: `missing Durable Object binding "${binding}"` } }, 500); + const session = decodeURIComponent(m[2]); + return ns.get(ns.idFromName(session)).fetch(req); + } + + return { fetch: fetchHandler, SessionDO }; +} + +function str(v: unknown): string { return typeof v === "string" ? v : ""; } diff --git a/sdks/langgraph/tsconfig.json b/sdks/langgraph/tsconfig.json new file mode 100644 index 00000000..c20e332d --- /dev/null +++ b/sdks/langgraph/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "types": ["node"] + }, + "include": ["src"] +} diff --git a/web/src/lib/runtimes.ts b/web/src/lib/runtimes.ts index 369b8737..9cf7b909 100644 --- a/web/src/lib/runtimes.ts +++ b/web/src/lib/runtimes.ts @@ -96,6 +96,7 @@ export const RUNTIME_LABELS: Record = { codex: 'Codex', pi: 'Pi', flue: 'Flue', + langgraph: 'LangGraph', hands: 'Hands', }