Repository: breadboard-ai/breadboard
Pull request #5589: Bgl sync script
Author: login: jameszhao00
Created: 2025-05-05T18:19:18Z | Merged: 2025-05-07T04:10:13Z

Safely(ish) sync bgl .ts code to .bgl.json, and one time regen bgl.json from TS code.

---

The changes across the `.bgl.json` files primarily consist of (Gemini generated change summary):

The primary changes in the `code` sections of your `.bgl.json` files are related to how ECMAScript (JavaScript) class features are being transpiled, along with some import modifications:

1.  **Native Private Class Members (`#field`, `#method()`):**
    *   **Before:** Private class fields and methods were transpiled using `WeakMap`s (e.g., `_McpClient_id = new WeakMap()`) and helper functions (e.g., `__classPrivateFieldSet`, `__classPrivateFieldGet`). This is typical for older ECMAScript targets like ES2020 when `useDefineForClassFields` is `false`.
    *   **Now:** The code uses the native `#` syntax for private class members (e.g., `class McpClient { #id = 0; ... this.#id ... }`). This indicates that the TypeScript compilation target is likely now `ES2022` or newer, or `ES2020` with `"useDefineForClassFields": true` in your tsconfig.json, which allows for this more modern and direct syntax.
    *   This change is visible in:
        *   `fetch-workbench.bgl.json` (for `McpClient`)
        *   `generate-text.bgl.json` (for `GenerateText` and `ChatToolImpl`)
        *   `go-over-list.bgl.json` (for `Runtime`)
        *   `google-drive.bgl.json` (for `SlideBuilder` and `TextStyles`)

2.  **Class Field Initialization:**
    *   Many class fields are now initialized directly as properties (e.g., `name = "Think as I go";`) rather than within the constructor (e.g., `constructor() { this.name = "Think as I go"; }`). This is a stylistic change often associated with modern class syntax.
    *   This is seen in classes within:
        *   `go-over-list.bgl.json` (e.g., `ParallelStrategist`, `SequentialStrategist`, `ThinkStrategist`, `ConversationalThinkStrategist`)

3.  **Nullish Coalescing Assignment (`??=`):**
    *   Some instances of `variable ?? (variable = defaultValue)` have been changed to the more concise `variable ??= defaultValue;`. This operator was standardized in ES2021.
    *   Examples can be found in:
        *   `audio-generator.bgl.json`
        *   `generate-text.bgl.json` (module: entry)
        *   `go-over-list.bgl.json` (planner-prompt modules)

4.  **Import Changes:**
    *   **Added Type/Utility Imports:** Several files now include imports like `import {} from "./types";` or `import {} from "./a2/common";`. These are often for type-only imports or to explicitly bring in modules.
    *   **New Utility Imports:** Some modules import additional utilities (e.g., `llm` from `./a2/utils`, `GeminiPrompt` from `./a2/gemini-prompt`).
    *   **Minor Import Syntax Changes:** Some imports like `import fetch from "@fetch"` changed to `import fetch, {} from "@fetch"`. This usually happens due to linters/formatters or if there's a plan to use named exports from that module later, even if none are used currently.
    *   The `types.bgl.json` files (in `generate-text` and `go-over-list`) had `export {};` removed, which is common if the file only contains type declarations and the explicit empty export is deemed unnecessary by the build tools.

In summary, the `.bgl.json` files now contain JavaScript code that leverages more modern ECMAScript features, particularly for class definitions. This makes the embedded code cleaner and more aligned with current JavaScript standards. This is almost certainly a result of changes to your TypeScript compilation settings.


Diff:
diff --git a/package-lock.json b/package-lock.json
index 2185f5957e3..53dcd69ca77 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,7 +14,6 @@
         "./packages/node-proxy-server/functions"
       ],
       "dependencies": {
-        "@rollup/rollup-linux-x64-gnu": "^4.40.2",
         "express": "^4.21.2",
         "json-schema": "^0.4.0",
         "litegraph.js": "^0.7.18",
@@ -5158,6 +5157,17 @@
         "@maxim_mazurok/gapi.client.drive-v3": "latest"
       }
     },
+    "node_modules/@types/glob": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz",
+      "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/minimatch": "^5.1.2",
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/google.picker": {
       "version": "0.0.50",
       "resolved": "https://registry.npmjs.org/@types/google.picker/-/google.picker-0.0.50.tgz",
@@ -5291,6 +5301,13 @@
       "version": "1.3.5",
       "license": "MIT"
     },
+    "node_modules/@types/minimatch": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
+      "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/mocha": {
       "version": "10.0.10",
       "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz",
@@ -23795,14 +23812,99 @@
       },
       "devDependencies": {
         "@google-cloud/firestore": "^7.11.0",
+        "@types/glob": "^8.1.0",
         "@types/node": "^22.0.0",
         "@typescript-eslint/eslint-plugin": "^7.18.0",
         "@typescript-eslint/parser": "^7.18.0",
         "dotenv": "^16.4.7",
+        "glob": "^11.0.2",
         "tsx": "^4.19.3",
         "typescript": "^5.8.3"
       }
     },
+    "packages/a2/node_modules/glob": {
+      "version": "11.0.2",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz",
+      "integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^4.0.1",
+        "minimatch": "^10.0.0",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^2.0.0"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "packages/a2/node_modules/jackspeak": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz",
+      "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "packages/a2/node_modules/lru-cache": {
+      "version": "11.1.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
+      "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "packages/a2/node_modules/minimatch": {
+      "version": "10.0.1",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
+      "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "packages/a2/node_modules/path-scurry": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
+      "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^11.0.0",
+        "minipass": "^7.1.2"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "packages/agent-kit": {
       "name": "@google-labs/agent-kit",
       "version": "0.18.0",
diff --git a/packages/a2/bgl/_connector-template.bgl.json b/packages/a2/bgl/_connector-template.bgl.json
index 3df2751ed0d..fed423939fc 100644
--- a/packages/a2/bgl/_connector-template.bgl.json
+++ b/packages/a2/bgl/_connector-template.bgl.json
@@ -53,7 +53,7 @@
   },
   "modules": {
     "configurator": {
-      "code": "/**\n * @fileoverview Add a description for your module here.\n */\nimport { err, ok } from \"./a2/utils\";\nimport { createConfigurator } from \"./a2/connector-manager\";\nimport read from \"@read\";\nimport write from \"@write\";\nexport { invoke as default, describe };\nconst CONNECTOR_TITLE = \"{{ title }}\";\nconst { invoke, describe } = createConfigurator({\n    title: CONNECTOR_TITLE,\n    initialize: async () => {\n        return { title: CONNECTOR_TITLE, configuration: {} };\n    },\n});\n",
+      "code": "/**\n * @fileoverview Add a description for your module here.\n */\nimport { createConfigurator } from \"./a2/connector-manager\";\nexport { invoke as default, describe };\nconst CONNECTOR_TITLE = \"{{ title }}\";\nconst { invoke, describe } = createConfigurator({\n    title: CONNECTOR_TITLE,\n    initialize: async () => {\n        return { title: CONNECTOR_TITLE, configuration: {} };\n    },\n});\n",
       "metadata": {
         "title": "configurator",
         "source": {
diff --git a/packages/a2/bgl/a2.bgl.json b/packages/a2/bgl/a2.bgl.json
index 5a5ed5d9024..4b6342a2a78 100644
--- a/packages/a2/bgl/a2.bgl.json
+++ b/packages/a2/bgl/a2.bgl.json
@@ -51,7 +51,7 @@
   },
   "modules": {
     "common": {
-      "code": "/**\n * @fileoverview Common types and code\n */\n",
+      "code": "/**\n * @fileoverview Common types and code\n */\nexport {};\n",
       "metadata": {
         "title": "common",
         "source": {
@@ -63,7 +63,7 @@
       }
     },
     "utils": {
-      "code": "/**\n * @fileoverview Common utils for manipulating LLM Content and other relevant types.\n */\nexport { isLLMContent, isLLMContentArray, toLLMContent, toInlineData, toLLMContentInline, toInlineReference, toLLMContentStored, toText, joinContent, contentToJSON, defaultLLMContent, endsWithRole, addUserTurn, isEmpty, isStoredData, llm, ok, err, generateId, mergeTextParts, toTextConcat, extractTextData, extractInlineData, extractMediaData, };\nfunction ok(o) {\n    return !(o && typeof o === \"object\" && \"$error\" in o);\n}\nfunction err($error) {\n    return { $error };\n}\nfunction mergeTextParts(parts, separator = \"\") {\n    const merged = [];\n    let text = \"\";\n    for (const part of parts) {\n        if (\"text\" in part) {\n            text += `${part.text}${separator}`;\n        }\n        else {\n            if (text) {\n                merged.push({ text });\n            }\n            text = \"\";\n            merged.push(part);\n        }\n    }\n    if (text) {\n        merged.push({ text });\n    }\n    return merged;\n}\nclass LLMTemplate {\n    strings;\n    values;\n    constructor(strings, values) {\n        this.strings = strings;\n        this.values = values;\n    }\n    asParts() {\n        return mergeTextParts(this.strings.flatMap((s, i) => {\n            let text = s;\n            const value = this.values.at(i);\n            if (value == undefined) {\n                return { text };\n            }\n            else if (typeof value === \"string\") {\n                text += value;\n                return { text };\n            }\n            else if (value instanceof LLMTemplate) {\n                return value.asParts();\n            }\n            else if (isLLMContent(value)) {\n                return [{ text }, ...value.parts];\n            }\n            else {\n                text += JSON.stringify(value);\n                return { text };\n            }\n        }));\n    }\n    asContent() {\n        const parts = this.asParts();\n        return { parts, role: \"user\" };\n    }\n}\nfunction llm(strings, ...values) {\n    return new LLMTemplate(strings, values);\n}\n/**\n * Copied from @google-labs/breadboard\n */\nfunction isLLMContent(nodeValue) {\n    if (typeof nodeValue !== \"object\" || !nodeValue)\n        return false;\n    if (nodeValue === null || nodeValue === undefined)\n        return false;\n    if (\"role\" in nodeValue && nodeValue.role === \"$metadata\") {\n        return true;\n    }\n    return \"parts\" in nodeValue && Array.isArray(nodeValue.parts);\n}\nfunction isLLMContentArray(nodeValue) {\n    if (!Array.isArray(nodeValue))\n        return false;\n    if (nodeValue.length === 0)\n        return true;\n    return isLLMContent(nodeValue.at(-1));\n}\nfunction toLLMContent(text, role = \"user\") {\n    return { parts: [{ text }], role };\n}\nfunction endsWithRole(c, role) {\n    const last = c.at(-1);\n    if (!last)\n        return false;\n    return last.role === role;\n}\nfunction isEmpty(c) {\n    if (!c.parts.length)\n        return true;\n    for (const part of c.parts) {\n        if (\"text\" in part) {\n            if (part.text.trim().length > 0)\n                return false;\n        }\n        else {\n            return false;\n        }\n    }\n    return true;\n}\nfunction isStoredData(c) {\n    const part = c.parts.at(-1);\n    if (!part) {\n        return false;\n    }\n    return \"storedData\" in part && part.storedData != null;\n}\nfunction toText(c) {\n    if (isLLMContent(c)) {\n        return contentToText(c);\n    }\n    const last = c.at(-1);\n    if (!last)\n        return \"\";\n    return contentToText(last).trim();\n    function contentToText(content) {\n        return content.parts\n            .map((part) => (\"text\" in part ? part.text : \"\"))\n            .join(\"\\n\\n\");\n    }\n}\nfunction contentToJSON(content) {\n    const part = content?.parts?.at(0);\n    if (!part || !(\"text\" in part)) {\n        throw new Error(\"Invalid response from Gemini\");\n    }\n    return JSON.parse(part.text);\n}\nfunction defaultLLMContent() {\n    return JSON.stringify({\n        parts: [{ text: \"\" }],\n        role: \"user\",\n    });\n}\nfunction joinContent(content, context, dropHistory = false) {\n    if (dropHistory && context) {\n        let last = context.at(-1);\n        let retainedContext;\n        if (last) {\n            retainedContext = [last];\n        }\n        return addUserTurn(content, retainedContext);\n    }\n    return addUserTurn(content, context);\n}\nfunction extractInlineData(context) {\n    const results = [];\n    for (let el of context) {\n        for (let part of el.parts) {\n            if (part) {\n                if (\"inlineData\" in part && part.inlineData) {\n                    results.push(toLLMContentInline(part.inlineData.mimeType, part.inlineData.data));\n                }\n            }\n        }\n    }\n    return results;\n}\nfunction extractMediaData(context) {\n    const results = [];\n    for (let el of context) {\n        for (let part of el.parts) {\n            if (part) {\n                if (\"inlineData\" in part && part.inlineData) {\n                    results.push(toLLMContentInline(part.inlineData.mimeType, part.inlineData.data));\n                }\n                if (\"storedData\" in part && part.storedData) {\n                    results.push(toLLMContentStored(part.storedData.mimeType, part.storedData.handle));\n                }\n            }\n        }\n    }\n    return results;\n}\nfunction extractTextData(context) {\n    const results = [];\n    for (let el of context) {\n        for (let part of el.parts) {\n            if (part) {\n                if (\"text\" in part && part.text) {\n                    results.push(toLLMContent(part.text));\n                }\n            }\n        }\n    }\n    return results;\n}\nfunction toTextConcat(content) {\n    return content.map(toText).join(\"\\n\\n\");\n}\nfunction addUserTurn(content, context) {\n    context ??= [];\n    const isString = typeof content === \"string\";\n    if (!endsWithRole(context, \"user\")) {\n        return [...context, isString ? toLLMContent(content) : content];\n    }\n    const last = context.at(-1);\n    if (isString) {\n        last.parts.push({ text: content });\n    }\n    else {\n        last.parts.push(...content.parts);\n    }\n    return context;\n}\nfunction toLLMContentInline(mimetype, value, role = \"user\") {\n    return {\n        parts: [\n            {\n                inlineData: {\n                    mimeType: mimetype,\n                    data: value,\n                },\n            },\n        ],\n        role,\n    };\n}\nfunction toLLMContentStored(mimetype, handle, role = \"user\") {\n    return {\n        parts: [\n            {\n                storedData: {\n                    mimeType: mimetype,\n                    handle: handle,\n                },\n            },\n        ],\n        role,\n    };\n}\nfunction toInlineData(c) {\n    if (isLLMContent(c)) {\n        return contentToInlineData(c);\n    }\n    const last = c.at(-1);\n    if (!last)\n        return null;\n    return contentToInlineData(last);\n    function contentToInlineData(content) {\n        const part = content.parts.at(-1);\n        if (!part)\n            return \"\";\n        return \"inlineData\" in part && part.inlineData ? part.inlineData : null;\n    }\n}\n// TODO(askerryryan): Move this to the middleware.\nfunction toInlineReference(c) {\n    const last = c.parts.at(-1);\n    if (last == undefined || !(\"storedData\" in last)) {\n        return toInlineData(c);\n    }\n    const blobId = last.storedData.handle.split(\"/\").slice(-1)[0];\n    const gcs_handle = \"bb-blob-store/\" + blobId;\n    return toInlineData(toLLMContentInline(\"text/gcs-path\", btoa(gcs_handle)));\n}\nexport function mergeContent(content, role) {\n    const parts = [];\n    for (const el of content) {\n        for (const part of el.parts) {\n            parts.push(part);\n        }\n    }\n    return {\n        parts: parts,\n        role: role,\n    };\n}\nfunction generateId() {\n    return Math.random().toString(36).substring(2, 5);\n}\n",
+      "code": "/**\n * @fileoverview Common utils for manipulating LLM Content and other relevant types.\n */\nexport { isLLMContent, isLLMContentArray, toLLMContent, toInlineData, toLLMContentInline, toInlineReference, toLLMContentStored, toText, joinContent, contentToJSON, defaultLLMContent, endsWithRole, addUserTurn, isEmpty, isStoredData, llm, ok, err, generateId, mergeTextParts, toTextConcat, extractTextData, extractInlineData, extractMediaData, };\nfunction ok(o) {\n    return !(o && typeof o === \"object\" && \"$error\" in o);\n}\nfunction err($error) {\n    return { $error };\n}\nfunction mergeTextParts(parts, separator = \"\") {\n    const merged = [];\n    let text = \"\";\n    for (const part of parts) {\n        if (\"text\" in part) {\n            text += `${part.text}${separator}`;\n        }\n        else {\n            if (text) {\n                merged.push({ text });\n            }\n            text = \"\";\n            merged.push(part);\n        }\n    }\n    if (text) {\n        merged.push({ text });\n    }\n    return merged;\n}\nclass LLMTemplate {\n    constructor(strings, values) {\n        this.strings = strings;\n        this.values = values;\n    }\n    asParts() {\n        return mergeTextParts(this.strings.flatMap((s, i) => {\n            let text = s;\n            const value = this.values.at(i);\n            if (value == undefined) {\n                return { text };\n            }\n            else if (typeof value === \"string\") {\n                text += value;\n                return { text };\n            }\n            else if (value instanceof LLMTemplate) {\n                return value.asParts();\n            }\n            else if (isLLMContent(value)) {\n                return [{ text }, ...value.parts];\n            }\n            else {\n                text += JSON.stringify(value);\n                return { text };\n            }\n        }));\n    }\n    asContent() {\n        const parts = this.asParts();\n        return { parts, role: \"user\" };\n    }\n}\nfunction llm(strings, ...values) {\n    return new LLMTemplate(strings, values);\n}\n/**\n * Copied from @google-labs/breadboard\n */\nfunction isLLMContent(nodeValue) {\n    if (typeof nodeValue !== \"object\" || !nodeValue)\n        return false;\n    if (nodeValue === null || nodeValue === undefined)\n        return false;\n    if (\"role\" in nodeValue && nodeValue.role === \"$metadata\") {\n        return true;\n    }\n    return \"parts\" in nodeValue && Array.isArray(nodeValue.parts);\n}\nfunction isLLMContentArray(nodeValue) {\n    if (!Array.isArray(nodeValue))\n        return false;\n    if (nodeValue.length === 0)\n        return true;\n    return isLLMContent(nodeValue.at(-1));\n}\nfunction toLLMContent(text, role = \"user\") {\n    return { parts: [{ text }], role };\n}\nfunction endsWithRole(c, role) {\n    const last = c.at(-1);\n    if (!last)\n        return false;\n    return last.role === role;\n}\nfunction isEmpty(c) {\n    if (!c.parts.length)\n        return true;\n    for (const part of c.parts) {\n        if (\"text\" in part) {\n            if (part.text.trim().length > 0)\n                return false;\n        }\n        else {\n            return false;\n        }\n    }\n    return true;\n}\nfunction isStoredData(c) {\n    const part = c.parts.at(-1);\n    if (!part) {\n        return false;\n    }\n    return \"storedData\" in part && part.storedData != null;\n}\nfunction toText(c) {\n    if (isLLMContent(c)) {\n        return contentToText(c);\n    }\n    const last = c.at(-1);\n    if (!last)\n        return \"\";\n    return contentToText(last).trim();\n    function contentToText(content) {\n        return content.parts\n            .map((part) => (\"text\" in part ? part.text : \"\"))\n            .join(\"\\n\\n\");\n    }\n}\nfunction contentToJSON(content) {\n    const part = content?.parts?.at(0);\n    if (!part || !(\"text\" in part)) {\n        throw new Error(\"Invalid response from Gemini\");\n    }\n    return JSON.parse(part.text);\n}\nfunction defaultLLMContent() {\n    return JSON.stringify({\n        parts: [{ text: \"\" }],\n        role: \"user\",\n    });\n}\nfunction joinContent(content, context, dropHistory = false) {\n    if (dropHistory && context) {\n        let last = context.at(-1);\n        let retainedContext;\n        if (last) {\n            retainedContext = [last];\n        }\n        return addUserTurn(content, retainedContext);\n    }\n    return addUserTurn(content, context);\n}\nfunction extractInlineData(context) {\n    const results = [];\n    for (let el of context) {\n        for (let part of el.parts) {\n            if (part) {\n                if (\"inlineData\" in part && part.inlineData) {\n                    results.push(toLLMContentInline(part.inlineData.mimeType, part.inlineData.data));\n                }\n            }\n        }\n    }\n    return results;\n}\nfunction extractMediaData(context) {\n    const results = [];\n    for (let el of context) {\n        for (let part of el.parts) {\n            if (part) {\n                if (\"inlineData\" in part && part.inlineData) {\n                    results.push(toLLMContentInline(part.inlineData.mimeType, part.inlineData.data));\n                }\n                if (\"storedData\" in part && part.storedData) {\n                    results.push(toLLMContentStored(part.storedData.mimeType, part.storedData.handle));\n                }\n            }\n        }\n    }\n    return results;\n}\nfunction extractTextData(context) {\n    const results = [];\n    for (let el of context) {\n        for (let part of el.parts) {\n            if (part) {\n                if (\"text\" in part && part.text) {\n                    results.push(toLLMContent(part.text));\n                }\n            }\n        }\n    }\n    return results;\n}\nfunction toTextConcat(content) {\n    return content.map(toText).join(\"\\n\\n\");\n}\nfunction addUserTurn(content, context) {\n    context ?? (context = []);\n    const isString = typeof content === \"string\";\n    if (!endsWithRole(context, \"user\")) {\n        return [...context, isString ? toLLMContent(content) : content];\n    }\n    const last = context.at(-1);\n    if (isString) {\n        last.parts.push({ text: content });\n    }\n    else {\n        last.parts.push(...content.parts);\n    }\n    return context;\n}\nfunction toLLMContentInline(mimetype, value, role = \"user\") {\n    return {\n        parts: [\n            {\n                inlineData: {\n                    mimeType: mimetype,\n                    data: value,\n                },\n            },\n        ],\n        role,\n    };\n}\nfunction toLLMContentStored(mimetype, handle, role = \"user\") {\n    return {\n        parts: [\n            {\n                storedData: {\n                    mimeType: mimetype,\n                    handle: handle,\n                },\n            },\n        ],\n        role,\n    };\n}\nfunction toInlineData(c) {\n    if (isLLMContent(c)) {\n        return contentToInlineData(c);\n    }\n    const last = c.at(-1);\n    if (!last)\n        return null;\n    return contentToInlineData(last);\n    function contentToInlineData(content) {\n        const part = content.parts.at(-1);\n        if (!part)\n            return \"\";\n        return \"inlineData\" in part && part.inlineData ? part.inlineData : null;\n    }\n}\n// TODO(askerryryan): Move this to the middleware.\nfunction toInlineReference(c) {\n    const last = c.parts.at(-1);\n    if (last == undefined || !(\"storedData\" in last)) {\n        return toInlineData(c);\n    }\n    const blobId = last.storedData.handle.split(\"/\").slice(-1)[0];\n    const gcs_handle = \"bb-blob-store/\" + blobId;\n    return toInlineData(toLLMContentInline(\"text/gcs-path\", btoa(gcs_handle)));\n}\nexport function mergeContent(content, role) {\n    const parts = [];\n    for (const el of content) {\n        for (const part of el.parts) {\n            parts.push(part);\n        }\n    }\n    return {\n        parts: parts,\n        role: role,\n    };\n}\nfunction generateId() {\n    return Math.random().toString(36).substring(2, 5);\n}\n",
       "metadata": {
         "title": "utils",
         "source": {
@@ -75,7 +75,7 @@
       }
     },
     "gemini": {
-      "code": "/**\n * @fileoverview Gemini Model Family.\n */\nimport fetch from \"@fetch\";\nimport secrets from \"@secrets\";\nimport output from \"@output\";\nimport { ok, err, isLLMContentArray } from \"./utils\";\nimport { flattenContext } from \"./lists\";\nconst defaultSafetySettings = () => [\n    {\n        category: \"HARM_CATEGORY_SEXUALLY_EXPLICIT\",\n        threshold: \"OFF\",\n    },\n    {\n        category: \"HARM_CATEGORY_HARASSMENT\",\n        threshold: \"OFF\",\n    },\n    {\n        category: \"HARM_CATEGORY_DANGEROUS_CONTENT\",\n        threshold: \"OFF\",\n    },\n];\nfunction endpointURL(model) {\n    // const $metadata = {\n    //   title: \"Get GEMINI_KEY\",\n    //   description: \"Getting GEMINI_KEY from secrets\",\n    // };\n    // const key = await secrets({ $metadata, keys: [\"GEMINI_KEY\"] });\n    return `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;\n}\nexport { invoke as default, describe, defaultSafetySettings };\nconst VALID_MODALITIES = [\"Text\", \"Text and Image\", \"Audio\"];\nconst MODELS = [\n    \"gemini-2.0-flash\",\n    \"gemini-2.0-flash-001\",\n    \"gemini-2.0-flash-lite-preview-02-05\",\n    \"gemini-2.0-flash-exp\",\n    \"gemini-2.0-flash-thinking-exp\",\n    \"gemini-2.0-flash-thinking-exp-01-21\",\n    \"gemini-2.0-pro-exp-02-05\",\n    \"gemini-1.5-flash-latest\",\n    \"gemini-1.5-pro-latest\",\n    \"gemini-exp-1206\",\n    \"gemini-exp-1121\",\n    \"learnlm-1.5-pro-experimental\",\n    \"gemini-1.5-pro-001\",\n    \"gemini-1.5-pro-002\",\n    \"gemini-1.5-pro-exp-0801\",\n    \"gemini-1.5-pro-exp-0827\",\n    \"gemini-1.5-flash-001\",\n    \"gemini-1.5-flash-002\",\n    \"gemini-1.5-flash-8b-exp-0924\",\n    \"gemini-1.5-flash-8b-exp-0827\",\n    \"gemini-1.5-flash-exp-0827\",\n];\nconst NO_RETRY_CODES = [400, 429, 404, 403];\n/**\n * Modifies the body to remove any\n * Breadboard-specific extensions to LLM Content\n */\nfunction conformBody(body) {\n    return {\n        ...body,\n        contents: flattenContext(body.contents.map((content) => {\n            return {\n                ...content,\n                parts: content.parts.map((part) => {\n                    if (\"json\" in part) {\n                        return { text: JSON.stringify(part.json) };\n                    }\n                    return part;\n                }),\n            };\n        }), true),\n    };\n}\nasync function getAccessToken() {\n    const signInSecretName = \"connection:$sign-in\";\n    const result = await secrets({ keys: [signInSecretName] });\n    return result[signInSecretName];\n}\nasync function callAPI(retries, model, body, $metadata) {\n    const accessToken = await getAccessToken();\n    let $error = \"Unknown error\";\n    while (retries) {\n        const result = await fetch({\n            $metadata,\n            url: endpointURL(model),\n            method: \"POST\",\n            headers: {\n                Authorization: `Bearer ${accessToken}`,\n            },\n            body: conformBody(body),\n        });\n        if (!ok(result)) {\n            // Fetch is a bit weird, because it returns various props\n            // along with the `$error`. Let's handle that here.\n            const { status, $error: errObject } = result;\n            if (!status) {\n                if (errObject)\n                    return { $error: errObject };\n                // This is not an error response, presume fatal error.\n                return { $error };\n            }\n            $error = maybeExtractError(errObject);\n            if (NO_RETRY_CODES.includes(status)) {\n                return { $error };\n            }\n        }\n        else {\n            const outputs = result.response;\n            const candidate = outputs.candidates?.at(0);\n            if (!candidate) {\n                return err(\"Unable to get a good response from Gemini\");\n            }\n            if (\"content\" in candidate) {\n                return outputs;\n            }\n            if (candidate.finishReason === \"IMAGE_SAFETY\") {\n                return err(\"The response candidate content was flagged for image safety reasons.\");\n            }\n        }\n        retries--;\n    }\n    return { $error };\n}\nfunction maybeExtractError(e) {\n    try {\n        const parsed = JSON.parse(e);\n        return parsed.error.message;\n    }\n    catch (error) {\n        return e;\n    }\n}\nfunction isEmptyLLMContent(content) {\n    if (!content || !content.parts || content.parts.length === 0)\n        return true;\n    return content.parts.every((part) => {\n        if (\"text\" in part) {\n            return !part.text?.trim();\n        }\n        return true;\n    });\n}\nfunction addModality(body, modality) {\n    if (!modality)\n        return;\n    switch (modality) {\n        case \"Text\":\n            // No change, defaults.\n            break;\n        case \"Text and Image\":\n            body.generationConfig ??= {};\n            body.generationConfig.responseModalities = [\"TEXT\", \"IMAGE\"];\n            break;\n        case \"Audio\":\n            body.generationConfig ??= {};\n            body.generationConfig.responseModalities = [\"AUDIO\"];\n            break;\n    }\n}\nfunction constructBody(context = [], systemInstruction, prompt, modality) {\n    const contents = [...context];\n    if (!isEmptyLLMContent(prompt)) {\n        contents.push(prompt);\n    }\n    const body = {\n        contents,\n        safetySettings: defaultSafetySettings(),\n    };\n    const canHaveSystemInstruction = modality === \"Text\";\n    if (!isEmptyLLMContent(systemInstruction) && canHaveSystemInstruction) {\n        body.systemInstruction = systemInstruction;\n    }\n    addModality(body, modality);\n    return body;\n}\nfunction augmentBody(body, systemInstruction, prompt, modality) {\n    if (!body.systemInstruction || !isEmptyLLMContent(systemInstruction)) {\n        body.systemInstruction = systemInstruction;\n    }\n    if (!isEmptyLLMContent(prompt)) {\n        body.contents = [...body.contents, prompt];\n    }\n    addModality(body, modality);\n    return body;\n}\nfunction validateInputs(inputs) {\n    if (\"body\" in inputs) {\n        return;\n    }\n    if (inputs.context) {\n        const { context } = inputs;\n        if (!Array.isArray(context)) {\n            return err(\"Incoming context must be an array.\");\n        }\n        if (!isLLMContentArray(context)) {\n            return err(\"Malformed incoming context\");\n        }\n        return;\n    }\n    return err(\"Either body or context is required\");\n}\nasync function invoke(inputs) {\n    const validatingInputs = validateInputs(inputs);\n    if (!ok(validatingInputs)) {\n        return validatingInputs;\n    }\n    let { model } = inputs;\n    if (!model) {\n        model = MODELS[0];\n    }\n    const { context, systemInstruction, prompt, modality, body, $metadata } = inputs;\n    // TODO: Make this configurable.\n    const retries = 5;\n    if (!(\"body\" in inputs)) {\n        // Public API is being used.\n        // Behave as if we're wired in.\n        const result = await callAPI(retries, model, constructBody(context, systemInstruction, prompt, modality));\n        if (!ok(result)) {\n            return result;\n        }\n        const content = result.candidates.at(0)?.content;\n        if (!content) {\n            return err(\"Unable to get a good response from Gemini\");\n        }\n        return { context: [...context, content] };\n    }\n    else {\n        // Private API is being used.\n        // Behave as if we're being invoked.\n        return callAPI(retries, model, augmentBody(body, systemInstruction, prompt, modality), $metadata);\n    }\n}\nasync function describe({ inputs }) {\n    const canHaveModalities = inputs.model === \"gemini-2.0-flash-exp\";\n    const canHaveSystemInstruction = !canHaveModalities || (canHaveModalities && inputs.modality == \"Text\");\n    const maybeAddSystemInstruction = canHaveSystemInstruction\n        ? {\n            systemInstruction: {\n                type: \"object\",\n                behavior: [\"llm-content\", \"config\"],\n                title: \"System Instruction\",\n                default: '{\"role\":\"user\",\"parts\":[{\"text\":\"\"}]}',\n                description: \"(Optional) Give the model additional context on what to do,\" +\n                    \"like specific rules/guidelines to adhere to or specify behavior\" +\n                    \"separate from the provided context\",\n            },\n        }\n        : {};\n    const maybeAddModalities = canHaveModalities\n        ? {\n            modality: {\n                type: \"string\",\n                enum: [...VALID_MODALITIES],\n                title: \"Output Modality\",\n                behavior: [\"config\"],\n                description: \"(Optional) Tell the model what kind of output you're looking for.\",\n            },\n        }\n        : {};\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                model: {\n                    type: \"string\",\n                    behavior: [\"config\"],\n                    title: \"Model Name\",\n                    enum: MODELS,\n                    default: MODELS[0],\n                },\n                prompt: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\"],\n                    title: \"Prompt\",\n                    default: '{\"role\":\"user\",\"parts\":[{\"text\":\"\"}]}',\n                    description: \"(Optional) A prompt. Will be added to the end of the the conversation context.\",\n                },\n                ...maybeAddSystemInstruction,\n                ...maybeAddModalities,\n                context: {\n                    type: \"array\",\n                    items: {\n                        type: \"object\",\n                        behavior: [\"llm-content\"],\n                    },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: {\n                        type: \"object\",\n                        behavior: [\"llm-content\"],\n                    },\n                    title: \"Context out\",\n                },\n            },\n        },\n        metadata: {\n            icon: \"generative\",\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Gemini Model Family.\n */\nimport fetch from \"@fetch\";\nimport secrets from \"@secrets\";\nimport { ok, err, isLLMContentArray } from \"./utils\";\nimport { flattenContext } from \"./lists\";\nconst defaultSafetySettings = () => [\n    {\n        category: \"HARM_CATEGORY_SEXUALLY_EXPLICIT\",\n        threshold: \"OFF\",\n    },\n    {\n        category: \"HARM_CATEGORY_HARASSMENT\",\n        threshold: \"OFF\",\n    },\n    {\n        category: \"HARM_CATEGORY_DANGEROUS_CONTENT\",\n        threshold: \"OFF\",\n    },\n];\nfunction endpointURL(model) {\n    // const $metadata = {\n    //   title: \"Get GEMINI_KEY\",\n    //   description: \"Getting GEMINI_KEY from secrets\",\n    // };\n    // const key = await secrets({ $metadata, keys: [\"GEMINI_KEY\"] });\n    return `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;\n}\nexport { invoke as default, describe, defaultSafetySettings };\nconst VALID_MODALITIES = [\"Text\", \"Text and Image\", \"Audio\"];\nconst MODELS = [\n    \"gemini-2.0-flash\",\n    \"gemini-2.0-flash-001\",\n    \"gemini-2.0-flash-lite-preview-02-05\",\n    \"gemini-2.0-flash-exp\",\n    \"gemini-2.0-flash-thinking-exp\",\n    \"gemini-2.0-flash-thinking-exp-01-21\",\n    \"gemini-2.0-pro-exp-02-05\",\n    \"gemini-1.5-flash-latest\",\n    \"gemini-1.5-pro-latest\",\n    \"gemini-exp-1206\",\n    \"gemini-exp-1121\",\n    \"learnlm-1.5-pro-experimental\",\n    \"gemini-1.5-pro-001\",\n    \"gemini-1.5-pro-002\",\n    \"gemini-1.5-pro-exp-0801\",\n    \"gemini-1.5-pro-exp-0827\",\n    \"gemini-1.5-flash-001\",\n    \"gemini-1.5-flash-002\",\n    \"gemini-1.5-flash-8b-exp-0924\",\n    \"gemini-1.5-flash-8b-exp-0827\",\n    \"gemini-1.5-flash-exp-0827\",\n];\nconst NO_RETRY_CODES = [400, 429, 404, 403];\n/**\n * Modifies the body to remove any\n * Breadboard-specific extensions to LLM Content\n */\nfunction conformBody(body) {\n    return {\n        ...body,\n        contents: flattenContext(body.contents.map((content) => {\n            return {\n                ...content,\n                parts: content.parts.map((part) => {\n                    if (\"json\" in part) {\n                        return { text: JSON.stringify(part.json) };\n                    }\n                    return part;\n                }),\n            };\n        }), true),\n    };\n}\nasync function getAccessToken() {\n    const signInSecretName = \"connection:$sign-in\";\n    const result = await secrets({ keys: [signInSecretName] });\n    return result[signInSecretName];\n}\nasync function callAPI(retries, model, body, $metadata) {\n    const accessToken = await getAccessToken();\n    let $error = \"Unknown error\";\n    while (retries) {\n        const result = await fetch({\n            $metadata,\n            url: endpointURL(model),\n            method: \"POST\",\n            headers: {\n                Authorization: `Bearer ${accessToken}`,\n            },\n            body: conformBody(body),\n        });\n        if (!ok(result)) {\n            // Fetch is a bit weird, because it returns various props\n            // along with the `$error`. Let's handle that here.\n            const { status, $error: errObject } = result;\n            if (!status) {\n                if (errObject)\n                    return { $error: errObject };\n                // This is not an error response, presume fatal error.\n                return { $error };\n            }\n            $error = maybeExtractError(errObject);\n            if (NO_RETRY_CODES.includes(status)) {\n                return { $error };\n            }\n        }\n        else {\n            const outputs = result.response;\n            const candidate = outputs.candidates?.at(0);\n            if (!candidate) {\n                return err(\"Unable to get a good response from Gemini\");\n            }\n            if (\"content\" in candidate) {\n                return outputs;\n            }\n            if (candidate.finishReason === \"IMAGE_SAFETY\") {\n                return err(\"The response candidate content was flagged for image safety reasons.\");\n            }\n        }\n        retries--;\n    }\n    return { $error };\n}\nfunction maybeExtractError(e) {\n    try {\n        const parsed = JSON.parse(e);\n        return parsed.error.message;\n    }\n    catch (error) {\n        return e;\n    }\n}\nfunction isEmptyLLMContent(content) {\n    if (!content || !content.parts || content.parts.length === 0)\n        return true;\n    return content.parts.every((part) => {\n        if (\"text\" in part) {\n            return !part.text?.trim();\n        }\n        return true;\n    });\n}\nfunction addModality(body, modality) {\n    if (!modality)\n        return;\n    switch (modality) {\n        case \"Text\":\n            // No change, defaults.\n            break;\n        case \"Text and Image\":\n            body.generationConfig ?? (body.generationConfig = {});\n            body.generationConfig.responseModalities = [\"TEXT\", \"IMAGE\"];\n            break;\n        case \"Audio\":\n            body.generationConfig ?? (body.generationConfig = {});\n            body.generationConfig.responseModalities = [\"AUDIO\"];\n            break;\n    }\n}\nfunction constructBody(context = [], systemInstruction, prompt, modality) {\n    const contents = [...context];\n    if (!isEmptyLLMContent(prompt)) {\n        contents.push(prompt);\n    }\n    const body = {\n        contents,\n        safetySettings: defaultSafetySettings(),\n    };\n    const canHaveSystemInstruction = modality === \"Text\";\n    if (!isEmptyLLMContent(systemInstruction) && canHaveSystemInstruction) {\n        body.systemInstruction = systemInstruction;\n    }\n    addModality(body, modality);\n    return body;\n}\nfunction augmentBody(body, systemInstruction, prompt, modality) {\n    if (!body.systemInstruction || !isEmptyLLMContent(systemInstruction)) {\n        body.systemInstruction = systemInstruction;\n    }\n    if (!isEmptyLLMContent(prompt)) {\n        body.contents = [...body.contents, prompt];\n    }\n    addModality(body, modality);\n    return body;\n}\nfunction validateInputs(inputs) {\n    if (\"body\" in inputs) {\n        return;\n    }\n    if (inputs.context) {\n        const { context } = inputs;\n        if (!Array.isArray(context)) {\n            return err(\"Incoming context must be an array.\");\n        }\n        if (!isLLMContentArray(context)) {\n            return err(\"Malformed incoming context\");\n        }\n        return;\n    }\n    return err(\"Either body or context is required\");\n}\nasync function invoke(inputs) {\n    const validatingInputs = validateInputs(inputs);\n    if (!ok(validatingInputs)) {\n        return validatingInputs;\n    }\n    let { model } = inputs;\n    if (!model) {\n        model = MODELS[0];\n    }\n    const { context, systemInstruction, prompt, modality, body, $metadata } = inputs;\n    // TODO: Make this configurable.\n    const retries = 5;\n    if (!(\"body\" in inputs)) {\n        // Public API is being used.\n        // Behave as if we're wired in.\n        const result = await callAPI(retries, model, constructBody(context, systemInstruction, prompt, modality));\n        if (!ok(result)) {\n            return result;\n        }\n        const content = result.candidates.at(0)?.content;\n        if (!content) {\n            return err(\"Unable to get a good response from Gemini\");\n        }\n        return { context: [...context, content] };\n    }\n    else {\n        // Private API is being used.\n        // Behave as if we're being invoked.\n        return callAPI(retries, model, augmentBody(body, systemInstruction, prompt, modality), $metadata);\n    }\n}\nasync function describe({ inputs }) {\n    const canHaveModalities = inputs.model === \"gemini-2.0-flash-exp\";\n    const canHaveSystemInstruction = !canHaveModalities || (canHaveModalities && inputs.modality == \"Text\");\n    const maybeAddSystemInstruction = canHaveSystemInstruction\n        ? {\n            systemInstruction: {\n                type: \"object\",\n                behavior: [\"llm-content\", \"config\"],\n                title: \"System Instruction\",\n                default: '{\"role\":\"user\",\"parts\":[{\"text\":\"\"}]}',\n                description: \"(Optional) Give the model additional context on what to do,\" +\n                    \"like specific rules/guidelines to adhere to or specify behavior\" +\n                    \"separate from the provided context\",\n            },\n        }\n        : {};\n    const maybeAddModalities = canHaveModalities\n        ? {\n            modality: {\n                type: \"string\",\n                enum: [...VALID_MODALITIES],\n                title: \"Output Modality\",\n                behavior: [\"config\"],\n                description: \"(Optional) Tell the model what kind of output you're looking for.\",\n            },\n        }\n        : {};\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                model: {\n                    type: \"string\",\n                    behavior: [\"config\"],\n                    title: \"Model Name\",\n                    enum: MODELS,\n                    default: MODELS[0],\n                },\n                prompt: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\"],\n                    title: \"Prompt\",\n                    default: '{\"role\":\"user\",\"parts\":[{\"text\":\"\"}]}',\n                    description: \"(Optional) A prompt. Will be added to the end of the the conversation context.\",\n                },\n                ...maybeAddSystemInstruction,\n                ...maybeAddModalities,\n                context: {\n                    type: \"array\",\n                    items: {\n                        type: \"object\",\n                        behavior: [\"llm-content\"],\n                    },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: {\n                        type: \"object\",\n                        behavior: [\"llm-content\"],\n                    },\n                    title: \"Context out\",\n                },\n            },\n        },\n        metadata: {\n            icon: \"generative\",\n        },\n    };\n}\n",
       "metadata": {
         "title": "Gemini",
         "source": {
@@ -87,7 +87,7 @@
       }
     },
     "entry": {
-      "code": "/**\n * @fileoverview Manages the entry point: describer, passing the inputs, etc.\n */\nimport {} from \"./common\";\nimport { ok, toLLMContent, defaultLLMContent } from \"./utils\";\nimport { Template } from \"./template\";\nimport { readSettings } from \"./settings\";\nexport { invoke as default, describe };\nasync function invoke({ context, \"p-chat\": chat, \"p-list\": makeList, description, ...params }) {\n    // Make sure it's a boolean.\n    chat = !!chat;\n    context ??= [];\n    const defaultModel = \"\";\n    const type = \"work\";\n    return {\n        context: {\n            id: Math.random().toString(36).substring(2, 5),\n            chat,\n            makeList,\n            listPath: [],\n            context,\n            userInputs: [],\n            defaultModel,\n            model: \"\",\n            description,\n            tools: [],\n            type,\n            work: [],\n            userEndedChat: false,\n            params,\n        },\n    };\n}\nasync function describe({ inputs: { description } }) {\n    const settings = await readSettings();\n    const experimental = ok(settings) && !!settings[\"Show Experimental Components\"];\n    const template = new Template(description);\n    let extra = {};\n    if (experimental) {\n        extra = {\n            \"p-chat\": {\n                type: \"boolean\",\n                title: \"Chat with User\",\n                behavior: [\"config\", \"hint-preview\"],\n                icon: \"chat\",\n                description: \"When checked, this step will chat with the user, asking to review work, requesting additional information, etc.\",\n            },\n            \"p-list\": {\n                type: \"boolean\",\n                title: \"Make a list\",\n                behavior: [\"config\", \"hint-preview\"],\n                icon: \"summarize\",\n                description: \"When checked, this step will try to create a list as its output. Make sure that the prompt asks for a list of some sort\",\n            },\n        };\n    }\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                description: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-preview\"],\n                    title: \"Instruction\",\n                    description: \"Give the model additional context on what to do, like specific rules/guidelines to adhere to or specify behavior separate from the provided context.\",\n                    default: defaultLLMContent(),\n                },\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                ...extra,\n                ...template.schemas(),\n            },\n            behavior: [\"at-wireable\"],\n            ...template.requireds(),\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"main-port\", \"hint-text\"],\n                },\n            },\n        },\n        title: \"Make Text\",\n        metadata: {\n            icon: \"generative-text\",\n            tags: [\"quick-access\", \"generative\"],\n            order: 1,\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Manages the entry point: describer, passing the inputs, etc.\n */\nimport { ok, defaultLLMContent } from \"./utils\";\nimport { Template } from \"./template\";\nimport { readSettings } from \"./settings\";\nexport { invoke as default, describe };\nasync function invoke({ context, \"p-chat\": chat, \"p-list\": makeList, description, ...params }) {\n    // Make sure it's a boolean.\n    chat = !!chat;\n    context ?? (context = []);\n    const defaultModel = \"\";\n    const type = \"work\";\n    return {\n        context: {\n            id: Math.random().toString(36).substring(2, 5),\n            chat,\n            makeList,\n            listPath: [],\n            context,\n            userInputs: [],\n            defaultModel,\n            model: \"\",\n            description,\n            tools: [],\n            type,\n            work: [],\n            userEndedChat: false,\n            params,\n        },\n    };\n}\nasync function describe({ inputs: { description } }) {\n    const settings = await readSettings();\n    const experimental = ok(settings) && !!settings[\"Show Experimental Components\"];\n    const template = new Template(description);\n    let extra = {};\n    if (experimental) {\n        extra = {\n            \"p-chat\": {\n                type: \"boolean\",\n                title: \"Chat with User\",\n                behavior: [\"config\", \"hint-preview\"],\n                icon: \"chat\",\n                description: \"When checked, this step will chat with the user, asking to review work, requesting additional information, etc.\",\n            },\n            \"p-list\": {\n                type: \"boolean\",\n                title: \"Make a list\",\n                behavior: [\"config\", \"hint-preview\"],\n                icon: \"summarize\",\n                description: \"When checked, this step will try to create a list as its output. Make sure that the prompt asks for a list of some sort\",\n            },\n        };\n    }\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                description: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-preview\"],\n                    title: \"Instruction\",\n                    description: \"Give the model additional context on what to do, like specific rules/guidelines to adhere to or specify behavior separate from the provided context.\",\n                    default: defaultLLMContent(),\n                },\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                ...extra,\n                ...template.schemas(),\n            },\n            behavior: [\"at-wireable\"],\n            ...template.requireds(),\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"main-port\", \"hint-text\"],\n                },\n            },\n        },\n        title: \"Make Text\",\n        metadata: {\n            icon: \"generative-text\",\n            tags: [\"quick-access\", \"generative\"],\n            order: 1,\n        },\n    };\n}\n",
       "metadata": {
         "title": "Make Text",
         "source": {
@@ -99,7 +99,7 @@
       }
     },
     "join": {
-      "code": "/**\n * @fileoverview Joins user input and Agent Context\n */\nimport {} from \"./common\";\nimport { isEmpty } from \"./utils\";\nimport { addContent } from \"./lists\";\nexport { invoke as default, describe };\nasync function invoke({ context, request }) {\n    context.userEndedChat = isEmpty(request);\n    context.userInputs.push(request);\n    if (!context.userEndedChat) {\n        context.work = addContent(context.work, request);\n    }\n    return { context };\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    title: \"Agent Context\",\n                    type: \"object\",\n                },\n                request: {\n                    title: \"User Input\",\n                    type: \"object\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    title: \"Agent Context\",\n                    type: \"object\",\n                },\n            },\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Joins user input and Agent Context\n */\nimport { isEmpty } from \"./utils\";\nimport { addContent } from \"./lists\";\nexport { invoke as default, describe };\nasync function invoke({ context, request }) {\n    context.userEndedChat = isEmpty(request);\n    context.userInputs.push(request);\n    if (!context.userEndedChat) {\n        context.work = addContent(context.work, request);\n    }\n    return { context };\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    title: \"Agent Context\",\n                    type: \"object\",\n                },\n                request: {\n                    title: \"User Input\",\n                    type: \"object\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    title: \"Agent Context\",\n                    type: \"object\",\n                },\n            },\n        },\n    };\n}\n",
       "metadata": {
         "title": "join",
         "source": {
@@ -123,7 +123,7 @@
       }
     },
     "tool-manager": {
-      "code": "/**\n * @fileoverview Manages tools.\n */\nimport describeGraph from \"@describe\";\nimport invokeGraph from \"@invoke\";\nimport { ok, err } from \"./utils\";\nimport {} from \"./gemini\";\nimport { ConnectorManager } from \"./connector-manager\";\nexport { ToolManager };\nclass ToolManager {\n    describerResultTransformer;\n    #hasSearch = false;\n    tools = new Map();\n    connectors = new Map();\n    errors = [];\n    constructor(describerResultTransformer) {\n        this.describerResultTransformer = describerResultTransformer;\n    }\n    #convertSchemas(schema) {\n        return toGeminiSchema(schema);\n        function toGeminiSchema(schema) {\n            switch (schema.type) {\n                case \"object\": {\n                    if (schema.behavior?.includes(\"llm-content\")) {\n                        return {\n                            type: \"string\",\n                            description: schema.description || schema.title,\n                        };\n                    }\n                    if (!schema.properties) {\n                        return { type: \"object\" };\n                    }\n                    return {\n                        type: \"object\",\n                        properties: Object.fromEntries(Object.entries(schema.properties).map(([name, schema]) => {\n                            return [name, toGeminiSchema(schema)];\n                        })),\n                        required: schema.required,\n                    };\n                }\n                case \"array\": {\n                    const items = schema.items;\n                    if (items.behavior?.includes(\"llm-content\")) {\n                        return {\n                            type: \"string\",\n                            description: schema.description,\n                        };\n                    }\n                    return {\n                        type: \"array\",\n                        items: toGeminiSchema(schema.items),\n                    };\n                }\n                default: {\n                    const geminiSchema = { ...schema };\n                    delete geminiSchema.format;\n                    delete geminiSchema.behavior;\n                    delete geminiSchema.examples;\n                    delete geminiSchema.default;\n                    delete geminiSchema.transient;\n                    if (!geminiSchema.description) {\n                        geminiSchema.description = geminiSchema.title;\n                    }\n                    delete geminiSchema.title;\n                    return geminiSchema;\n                }\n            }\n        }\n    }\n    #toName(title) {\n        return title ? title.replace(/\\W/g, \"_\") : \"function\";\n    }\n    addSearch() {\n        this.#hasSearch = true;\n    }\n    #createToolHandle(url, description, passContext, connector) {\n        const name = this.#toName(description.title);\n        const functionDeclaration = {\n            name,\n            description: description.description || \"\",\n        };\n        const parameters = this.#convertSchemas(description.inputSchema);\n        if (parameters.properties) {\n            functionDeclaration.parameters = parameters;\n        }\n        return [name, { tool: functionDeclaration, url, passContext, connector }];\n    }\n    #addOneTool(url, description, passContext, connector) {\n        const [name, handle] = this.#createToolHandle(url, description, passContext, connector);\n        this.tools.set(name, handle);\n        return description.title || name;\n    }\n    addCustomTool(name, handle) {\n        this.tools.set(name, handle);\n    }\n    async addTool(url, instance) {\n        if (instance) {\n            // This is a connector.\n            const connector = new ConnectorManager({ path: instance });\n            const tools = await connector.listTools();\n            if (!ok(tools))\n                return tools;\n            for (const tool of tools) {\n                const { url, description } = tool;\n                this.#addOneTool(url, description, false, connector);\n            }\n            // Return empty string, which will inform the\n            // substitution machinery to just reuse title.\n            return \"\";\n        }\n        let description = (await describeGraph({\n            url,\n        }));\n        let passContext = false;\n        if (!ok(description))\n            return description;\n        // TODO: Remove this altogether?\n        // Let's see if there are exports. If yes, let's add the exports\n        // instead of the tool.\n        if (description.exports) {\n            let connector = null;\n            if (description.metadata?.tags?.includes(\"connector\")) {\n                // This is a connector\n                connector = { tools: new Map() };\n            }\n            Object.entries(description.exports).forEach(([id, exportDescription]) => {\n                // TODO: Figure out what to do with passContext\n                const idAndHandle = this.#createToolHandle(id, exportDescription, passContext);\n                const [name, handle] = idAndHandle;\n                console.log(\"EXPORT DESCRIPTION\", exportDescription);\n                if (connector) {\n                    if (exportDescription.metadata?.tags?.includes(\"connector-configure\")) {\n                        connector.configure = idAndHandle;\n                        return;\n                    }\n                    else if (exportDescription.metadata?.tags?.includes(\"connector-load\")) {\n                        connector.load = idAndHandle;\n                        return;\n                    }\n                    else {\n                        connector.tools.set(name, handle);\n                    }\n                }\n                this.tools.set(name, handle);\n            });\n            return this.#toName(description.title);\n        }\n        // Otherwise, let's add the tool itself.\n        if (this.describerResultTransformer) {\n            const transforming = await this.describerResultTransformer.transform(description);\n            if (!ok(transforming))\n                return transforming;\n            if (transforming) {\n                description = transforming;\n                passContext = true;\n            }\n        }\n        return this.#addOneTool(url, description, passContext);\n    }\n    async initialize(tools) {\n        if (!tools) {\n            return true;\n        }\n        let hasInvalidTools = false;\n        for (const tool of tools) {\n            const url = typeof tool === \"string\" ? tool : tool.url;\n            const description = (await describeGraph({\n                url,\n            }));\n            if (!ok(description)) {\n                this.errors.push(description.$error);\n                // Invalid tool, skip\n                hasInvalidTools = true;\n                continue;\n            }\n            const parameters = this.#convertSchemas(description.inputSchema);\n            const name = this.#toName(description.title);\n            const functionDeclaration = {\n                name,\n                description: description.description || \"\",\n                parameters,\n            };\n            this.tools.set(name, {\n                tool: functionDeclaration,\n                url,\n                passContext: false,\n            });\n        }\n        return !hasInvalidTools;\n    }\n    async processResponse(response, callTool) {\n        for (const part of response.parts) {\n            if (\"functionCall\" in part) {\n                const { args, name } = part.functionCall;\n                const handle = this.tools.get(name);\n                if (handle) {\n                    if (handle.invoke) {\n                        await handle.invoke(args);\n                    }\n                    else {\n                        const { url, passContext, connector } = handle;\n                        if (connector) {\n                            await connector.invokeTool(name, args, callTool);\n                        }\n                        else {\n                            await callTool(url, part.functionCall.args, passContext, name);\n                        }\n                    }\n                }\n            }\n        }\n    }\n    hasTools() {\n        return this.tools.size !== 0;\n    }\n    list() {\n        const declaration = {};\n        const entries = [...this.tools.entries()];\n        if (entries.length !== 0) {\n            declaration.functionDeclarations = entries.map(([, value]) => value.tool);\n        }\n        if (this.#hasSearch) {\n            declaration.googleSearch = {};\n        }\n        if (Object.keys(declaration).length === 0)\n            return [];\n        return [declaration];\n    }\n}\n",
+      "code": "/**\n * @fileoverview Manages tools.\n */\nvar __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {\n    if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\n    return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\n};\nvar __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n    return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n};\nvar _ToolManager_instances, _ToolManager_hasSearch, _ToolManager_convertSchemas, _ToolManager_toName, _ToolManager_createToolHandle, _ToolManager_addOneTool;\nimport describeGraph from \"@describe\";\nimport { ok } from \"./utils\";\nimport { ConnectorManager } from \"./connector-manager\";\nexport { ToolManager };\nclass ToolManager {\n    constructor(describerResultTransformer) {\n        _ToolManager_instances.add(this);\n        this.describerResultTransformer = describerResultTransformer;\n        _ToolManager_hasSearch.set(this, false);\n        this.tools = new Map();\n        this.connectors = new Map();\n        this.errors = [];\n    }\n    addSearch() {\n        __classPrivateFieldSet(this, _ToolManager_hasSearch, true, \"f\");\n    }\n    addCustomTool(name, handle) {\n        this.tools.set(name, handle);\n    }\n    async addTool(url, instance) {\n        if (instance) {\n            // This is a connector.\n            const connector = new ConnectorManager({ path: instance });\n            const tools = await connector.listTools();\n            if (!ok(tools))\n                return tools;\n            for (const tool of tools) {\n                const { url, description } = tool;\n                __classPrivateFieldGet(this, _ToolManager_instances, \"m\", _ToolManager_addOneTool).call(this, url, description, false, connector);\n            }\n            // Return empty string, which will inform the\n            // substitution machinery to just reuse title.\n            return \"\";\n        }\n        let description = (await describeGraph({\n            url,\n        }));\n        let passContext = false;\n        if (!ok(description))\n            return description;\n        // TODO: Remove this altogether?\n        // Let's see if there are exports. If yes, let's add the exports\n        // instead of the tool.\n        if (description.exports) {\n            let connector = null;\n            if (description.metadata?.tags?.includes(\"connector\")) {\n                // This is a connector\n                connector = { tools: new Map() };\n            }\n            Object.entries(description.exports).forEach(([id, exportDescription]) => {\n                // TODO: Figure out what to do with passContext\n                const idAndHandle = __classPrivateFieldGet(this, _ToolManager_instances, \"m\", _ToolManager_createToolHandle).call(this, id, exportDescription, passContext);\n                const [name, handle] = idAndHandle;\n                console.log(\"EXPORT DESCRIPTION\", exportDescription);\n                if (connector) {\n                    if (exportDescription.metadata?.tags?.includes(\"connector-configure\")) {\n                        connector.configure = idAndHandle;\n                        return;\n                    }\n                    else if (exportDescription.metadata?.tags?.includes(\"connector-load\")) {\n                        connector.load = idAndHandle;\n                        return;\n                    }\n                    else {\n                        connector.tools.set(name, handle);\n                    }\n                }\n                this.tools.set(name, handle);\n            });\n            return __classPrivateFieldGet(this, _ToolManager_instances, \"m\", _ToolManager_toName).call(this, description.title);\n        }\n        // Otherwise, let's add the tool itself.\n        if (this.describerResultTransformer) {\n            const transforming = await this.describerResultTransformer.transform(description);\n            if (!ok(transforming))\n                return transforming;\n            if (transforming) {\n                description = transforming;\n                passContext = true;\n            }\n        }\n        return __classPrivateFieldGet(this, _ToolManager_instances, \"m\", _ToolManager_addOneTool).call(this, url, description, passContext);\n    }\n    async initialize(tools) {\n        if (!tools) {\n            return true;\n        }\n        let hasInvalidTools = false;\n        for (const tool of tools) {\n            const url = typeof tool === \"string\" ? tool : tool.url;\n            const description = (await describeGraph({\n                url,\n            }));\n            if (!ok(description)) {\n                this.errors.push(description.$error);\n                // Invalid tool, skip\n                hasInvalidTools = true;\n                continue;\n            }\n            const parameters = __classPrivateFieldGet(this, _ToolManager_instances, \"m\", _ToolManager_convertSchemas).call(this, description.inputSchema);\n            const name = __classPrivateFieldGet(this, _ToolManager_instances, \"m\", _ToolManager_toName).call(this, description.title);\n            const functionDeclaration = {\n                name,\n                description: description.description || \"\",\n                parameters,\n            };\n            this.tools.set(name, {\n                tool: functionDeclaration,\n                url,\n                passContext: false,\n            });\n        }\n        return !hasInvalidTools;\n    }\n    async processResponse(response, callTool) {\n        for (const part of response.parts) {\n            if (\"functionCall\" in part) {\n                const { args, name } = part.functionCall;\n                const handle = this.tools.get(name);\n                if (handle) {\n                    if (handle.invoke) {\n                        await handle.invoke(args);\n                    }\n                    else {\n                        const { url, passContext, connector } = handle;\n                        if (connector) {\n                            await connector.invokeTool(name, args, callTool);\n                        }\n                        else {\n                            await callTool(url, part.functionCall.args, passContext, name);\n                        }\n                    }\n                }\n            }\n        }\n    }\n    hasTools() {\n        return this.tools.size !== 0;\n    }\n    list() {\n        const declaration = {};\n        const entries = [...this.tools.entries()];\n        if (entries.length !== 0) {\n            declaration.functionDeclarations = entries.map(([, value]) => value.tool);\n        }\n        if (__classPrivateFieldGet(this, _ToolManager_hasSearch, \"f\")) {\n            declaration.googleSearch = {};\n        }\n        if (Object.keys(declaration).length === 0)\n            return [];\n        return [declaration];\n    }\n}\n_ToolManager_hasSearch = new WeakMap(), _ToolManager_instances = new WeakSet(), _ToolManager_convertSchemas = function _ToolManager_convertSchemas(schema) {\n    return toGeminiSchema(schema);\n    function toGeminiSchema(schema) {\n        switch (schema.type) {\n            case \"object\": {\n                if (schema.behavior?.includes(\"llm-content\")) {\n                    return {\n                        type: \"string\",\n                        description: schema.description || schema.title,\n                    };\n                }\n                if (!schema.properties) {\n                    return { type: \"object\" };\n                }\n                return {\n                    type: \"object\",\n                    properties: Object.fromEntries(Object.entries(schema.properties).map(([name, schema]) => {\n                        return [name, toGeminiSchema(schema)];\n                    })),\n                    required: schema.required,\n                };\n            }\n            case \"array\": {\n                const items = schema.items;\n                if (items.behavior?.includes(\"llm-content\")) {\n                    return {\n                        type: \"string\",\n                        description: schema.description,\n                    };\n                }\n                return {\n                    type: \"array\",\n                    items: toGeminiSchema(schema.items),\n                };\n            }\n            default: {\n                const geminiSchema = { ...schema };\n                delete geminiSchema.format;\n                delete geminiSchema.behavior;\n                delete geminiSchema.examples;\n                delete geminiSchema.default;\n                delete geminiSchema.transient;\n                if (!geminiSchema.description) {\n                    geminiSchema.description = geminiSchema.title;\n                }\n                delete geminiSchema.title;\n                return geminiSchema;\n            }\n        }\n    }\n}, _ToolManager_toName = function _ToolManager_toName(title) {\n    return title ? title.replace(/\\W/g, \"_\") : \"function\";\n}, _ToolManager_createToolHandle = function _ToolManager_createToolHandle(url, description, passContext, connector) {\n    const name = __classPrivateFieldGet(this, _ToolManager_instances, \"m\", _ToolManager_toName).call(this, description.title);\n    const functionDeclaration = {\n        name,\n        description: description.description || \"\",\n    };\n    const parameters = __classPrivateFieldGet(this, _ToolManager_instances, \"m\", _ToolManager_convertSchemas).call(this, description.inputSchema);\n    if (parameters.properties) {\n        functionDeclaration.parameters = parameters;\n    }\n    return [name, { tool: functionDeclaration, url, passContext, connector }];\n}, _ToolManager_addOneTool = function _ToolManager_addOneTool(url, description, passContext, connector) {\n    const [name, handle] = __classPrivateFieldGet(this, _ToolManager_instances, \"m\", _ToolManager_createToolHandle).call(this, url, description, passContext, connector);\n    this.tools.set(name, handle);\n    return description.title || name;\n};\n",
       "metadata": {
         "title": "tool-manager",
         "source": {
@@ -135,7 +135,7 @@
       }
     },
     "worker-worker": {
-      "code": "/**\n * @fileoverview Performs assigned task. Part of the worker.\n */\nimport invokeBoard from \"@invoke\";\nimport output from \"@output\";\nimport { toText, toLLMContent, ok, err, llm, generateId } from \"./utils\";\nimport { defaultSafetySettings, } from \"./gemini\";\nimport { callGemini } from \"./gemini-client\";\nimport { GeminiPrompt } from \"./gemini-prompt\";\nimport { StructuredResponse } from \"./structured-response\";\nimport { ToolManager } from \"./tool-manager\";\nimport { fanOutContext, toList, listPrompt, listSchema, hasLists, } from \"./lists\";\nexport { invoke as default, describe };\nfunction computeWorkMode(tools, summarize) {\n    if (summarize) {\n        return \"summarize\";\n    }\n    if (tools.length > 0) {\n        return \"call-tools\";\n    }\n    return \"generate\";\n}\nasync function callTools(inputs, model, tools, retries) {\n    inputs.body.tools = tools;\n    inputs.body.toolConfig = {\n        functionCallingConfig: {\n            mode: \"ANY\",\n        },\n    };\n    const response = await callGemini(inputs, model, (response) => {\n        const r = response;\n        if (r.candidates?.at(0)?.content)\n            return;\n        return err(\"No content\");\n    }, retries);\n    if (!ok(response)) {\n        return toLLMContent(\"TODO: Handle Gemini error response\");\n    }\n    const r = response;\n    return r.candidates?.at(0)?.content || toLLMContent(\"No valid response\");\n}\nasync function generate(inputs, model, responseManager, retries) {\n    const response = await callGemini(inputs, model, (response) => {\n        return responseManager.parse(response);\n    }, retries);\n    if (!ok(response)) {\n        return response;\n    }\n    else {\n        return {\n            product: toLLMContent(responseManager.body, \"model\"),\n            response: responseManager.response,\n        };\n    }\n}\nasync function invoke({ id, work: context, description: instruction, model, toolManager, summarize, chat, makeList, }) {\n    // TODO: Make this a parameter.\n    const retries = 5;\n    const tools = toolManager.list();\n    const mode = computeWorkMode(tools, summarize);\n    const responseManager = new StructuredResponse(id, chat);\n    if (mode === \"call-tools\") {\n        const product = await callTools({\n            body: {\n                contents: [...context, prompt(instruction, mode, chat)],\n                systemInstruction: responseManager.instruction(),\n                safetySettings: defaultSafetySettings(),\n            },\n        }, model, tools, retries);\n        return { product };\n    }\n    else {\n        let product = null;\n        if (makeList) {\n            const generating = await new GeminiPrompt({\n                body: {\n                    contents: [...context, listPrompt(prompt(instruction, mode, chat))],\n                    safetySettings: defaultSafetySettings(),\n                    generationConfig: {\n                        responseSchema: listSchema(),\n                        responseMimeType: \"application/json\",\n                    },\n                },\n            }, {\n                toolManager,\n            }).invoke();\n            if (!ok(generating))\n                return generating;\n            const list = toList(generating.last);\n            if (!ok(list))\n                return list;\n            product = list;\n        }\n        else {\n            const result = await generate({\n                body: {\n                    contents: responseManager.addPrompt(context, prompt(instruction, mode, chat)),\n                    systemInstruction: responseManager.instruction(),\n                    safetySettings: defaultSafetySettings(),\n                },\n            }, model, responseManager, retries);\n            if (\"$error\" in result) {\n                return result;\n            }\n            product = result.product;\n        }\n        return { product, epilog: responseManager.epilog };\n    }\n}\nfunction prompt(description, mode, chat) {\n    const preamble = llm `\n${description}\n\n`;\n    const postamble = `\n\nToday is ${new Date().toLocaleString(\"en-US\", {\n        month: \"long\",\n        day: \"numeric\",\n        year: \"numeric\",\n        hour: \"numeric\",\n        minute: \"2-digit\",\n    })}`;\n    switch (mode) {\n        case \"summarize\":\n            return llm ` \n${preamble}\nSummarize the research results to fulfill the specified task.\n${postamble}`.asContent();\n        case \"call-tools\":\n            return llm `\n${preamble}\nGenerate multiple function calls to fulfill the specified task.\n${postamble}`.asContent();\n        case \"generate\":\n            return llm `\n${preamble}\nProvide the response that fulfills the specified task.\n${postamble}`.asContent();\n    }\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                work: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Work\",\n                },\n                description: {\n                    type: \"object\",\n                    behavior: [\"llm-content\"],\n                    title: \"Job Description\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"object\",\n                    behavior: [\"llm-content\"],\n                    title: \"Work Product\",\n                },\n            },\n        },\n    };\n}\n",
+      "code": "import { toLLMContent, ok, err, llm } from \"./utils\";\nimport { defaultSafetySettings, } from \"./gemini\";\nimport { callGemini } from \"./gemini-client\";\nimport { GeminiPrompt } from \"./gemini-prompt\";\nimport { StructuredResponse } from \"./structured-response\";\nimport { toList, listPrompt, listSchema, } from \"./lists\";\nexport { invoke as default, describe };\nfunction computeWorkMode(tools, summarize) {\n    if (summarize) {\n        return \"summarize\";\n    }\n    if (tools.length > 0) {\n        return \"call-tools\";\n    }\n    return \"generate\";\n}\nasync function callTools(inputs, model, tools, retries) {\n    inputs.body.tools = tools;\n    inputs.body.toolConfig = {\n        functionCallingConfig: {\n            mode: \"ANY\",\n        },\n    };\n    const response = await callGemini(inputs, model, (response) => {\n        const r = response;\n        if (r.candidates?.at(0)?.content)\n            return;\n        return err(\"No content\");\n    }, retries);\n    if (!ok(response)) {\n        return toLLMContent(\"TODO: Handle Gemini error response\");\n    }\n    const r = response;\n    return r.candidates?.at(0)?.content || toLLMContent(\"No valid response\");\n}\nasync function generate(inputs, model, responseManager, retries) {\n    const response = await callGemini(inputs, model, (response) => {\n        return responseManager.parse(response);\n    }, retries);\n    if (!ok(response)) {\n        return response;\n    }\n    else {\n        return {\n            product: toLLMContent(responseManager.body, \"model\"),\n            response: responseManager.response,\n        };\n    }\n}\nasync function invoke({ id, work: context, description: instruction, model, toolManager, summarize, chat, makeList, }) {\n    // TODO: Make this a parameter.\n    const retries = 5;\n    const tools = toolManager.list();\n    const mode = computeWorkMode(tools, summarize);\n    const responseManager = new StructuredResponse(id, chat);\n    if (mode === \"call-tools\") {\n        const product = await callTools({\n            body: {\n                contents: [...context, prompt(instruction, mode, chat)],\n                systemInstruction: responseManager.instruction(),\n                safetySettings: defaultSafetySettings(),\n            },\n        }, model, tools, retries);\n        return { product };\n    }\n    else {\n        let product = null;\n        if (makeList) {\n            const generating = await new GeminiPrompt({\n                body: {\n                    contents: [...context, listPrompt(prompt(instruction, mode, chat))],\n                    safetySettings: defaultSafetySettings(),\n                    generationConfig: {\n                        responseSchema: listSchema(),\n                        responseMimeType: \"application/json\",\n                    },\n                },\n            }, {\n                toolManager,\n            }).invoke();\n            if (!ok(generating))\n                return generating;\n            const list = toList(generating.last);\n            if (!ok(list))\n                return list;\n            product = list;\n        }\n        else {\n            const result = await generate({\n                body: {\n                    contents: responseManager.addPrompt(context, prompt(instruction, mode, chat)),\n                    systemInstruction: responseManager.instruction(),\n                    safetySettings: defaultSafetySettings(),\n                },\n            }, model, responseManager, retries);\n            if (\"$error\" in result) {\n                return result;\n            }\n            product = result.product;\n        }\n        return { product, epilog: responseManager.epilog };\n    }\n}\nfunction prompt(description, mode, chat) {\n    const preamble = llm `\n${description}\n\n`;\n    const postamble = `\n\nToday is ${new Date().toLocaleString(\"en-US\", {\n        month: \"long\",\n        day: \"numeric\",\n        year: \"numeric\",\n        hour: \"numeric\",\n        minute: \"2-digit\",\n    })}`;\n    switch (mode) {\n        case \"summarize\":\n            return llm ` \n${preamble}\nSummarize the research results to fulfill the specified task.\n${postamble}`.asContent();\n        case \"call-tools\":\n            return llm `\n${preamble}\nGenerate multiple function calls to fulfill the specified task.\n${postamble}`.asContent();\n        case \"generate\":\n            return llm `\n${preamble}\nProvide the response that fulfills the specified task.\n${postamble}`.asContent();\n    }\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                work: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Work\",\n                },\n                description: {\n                    type: \"object\",\n                    behavior: [\"llm-content\"],\n                    title: \"Job Description\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"object\",\n                    behavior: [\"llm-content\"],\n                    title: \"Work Product\",\n                },\n            },\n        },\n    };\n}\n",
       "metadata": {
         "title": "worker-worker",
         "source": {
@@ -147,7 +147,7 @@
       }
     },
     "gemini-client": {
-      "code": "import invokeGemini, {} from \"./gemini\";\nimport { ok, err } from \"./utils\";\nexport { callGemini };\nasync function callGemini(inputs, model, validator, retries) {\n    // TODO: Add more nuanced logic around retries\n    for (let i = 0; i < retries; ++i) {\n        const nextStep = i == retries ? \"bailing\" : \"will retry\";\n        const response = await invokeGemini(inputs);\n        if (!ok(response)) {\n            console.error(`Error from model, ${nextStep}`);\n            return response;\n        }\n        else {\n            const validating = validator(response);\n            if (!ok(validating)) {\n                console.error(`Validation error, ${nextStep}`, validating.$error);\n                continue;\n            }\n            return response;\n        }\n    }\n    return err(`Failed to get valid response after ${retries} tries`);\n}\n",
+      "code": "import invokeGemini from \"./gemini\";\nimport { ok, err } from \"./utils\";\nexport { callGemini };\nasync function callGemini(inputs, model, validator, retries) {\n    // TODO: Add more nuanced logic around retries\n    for (let i = 0; i < retries; ++i) {\n        const nextStep = i == retries ? \"bailing\" : \"will retry\";\n        const response = await invokeGemini(inputs);\n        if (!ok(response)) {\n            console.error(`Error from model, ${nextStep}`);\n            return response;\n        }\n        else {\n            const validating = validator(response);\n            if (!ok(validating)) {\n                console.error(`Validation error, ${nextStep}`, validating.$error);\n                continue;\n            }\n            return response;\n        }\n    }\n    return err(`Failed to get valid response after ${retries} tries`);\n}\n",
       "metadata": {
         "title": "gemini-client",
         "source": {
@@ -159,7 +159,7 @@
       }
     },
     "agent-main": {
-      "code": "/**\n * @fileoverview The main body of the agent\n */\nimport output from \"@output\";\nimport {} from \"./common\";\nimport { ToolManager } from \"./tool-manager\";\nimport workerWorker from \"./worker-worker\";\nimport { report } from \"./output\";\nimport { defaultLLMContent, toText, err, endsWithRole, ok, llm } from \"./utils\";\nimport invokeGraph from \"@invoke\";\nimport { Template } from \"./template\";\nimport { ArgumentNameGenerator } from \"./introducer\";\nimport { ListExpander } from \"./lists\";\nexport { invoke as default, describe };\nfunction toLLMContent(text, role = \"user\") {\n    return { parts: [{ text }], role };\n}\nasync function invoke({ context }) {\n    console.log(\"AGENT MAIN\", context);\n    let { id, description, context: initialContext, model, defaultModel, tools, chat, makeList, work: workContext, params, } = context;\n    if (!description) {\n        const $error = \"No instruction supplied\";\n        await report({\n            actor: \"Text Generator\",\n            name: $error,\n            category: \"Runtime error\",\n            details: `In order to run, I need to have an instruction.`,\n        });\n        return { $error };\n    }\n    const template = new Template(description);\n    const toolManager = new ToolManager(new ArgumentNameGenerator());\n    const substituting = await template.substitute(params, async ({ path: url, instance }) => toolManager.addTool(url, instance));\n    if (!ok(substituting)) {\n        return substituting;\n    }\n    description = substituting;\n    if (!(await toolManager.initialize(tools))) {\n        const $error = `Problem initializing tools. \nThe following errors were encountered: ${toolManager.errors.join(\",\")}`;\n        console.error(\"MAIN ERROR\", $error, toolManager.errors);\n        return { $error };\n    }\n    const { userEndedChat, userInputs, last } = context;\n    if (userEndedChat) {\n        if (!last) {\n            return err(\"Chat ended without any work\");\n        }\n        return {\n            done: [...initialContext, last],\n        };\n    }\n    let epilog;\n    const result = await new ListExpander(description, workContext.length > 0 ? workContext : initialContext).map(async (description, work, isList) => {\n        // Disallow making nested lists\n        const disallowNestedMakeList = makeList && !isList;\n        // 1) Make first attempt to make text\n        const response = await workerWorker({\n            id,\n            description,\n            work,\n            model,\n            toolManager,\n            summarize: false,\n            chat,\n            makeList: disallowNestedMakeList,\n        });\n        if (!ok(response)) {\n            console.error(\"ERROR FROM WORKER\", response.$error);\n            return response;\n        }\n        const workerResponse = response.product;\n        epilog = response.epilog;\n        // 2) Call tools\n        const toolResults = [];\n        // TODO: Convert to GeminiPrompt. Somebody. Please.\n        const errors = [];\n        await toolManager.processResponse(workerResponse, async ($board, args) => {\n            const result = await invokeGraph({\n                $board,\n                ...args,\n            });\n            if (\"$error\" in result) {\n                errors.push(result.$error);\n            }\n            toolResults.push(result);\n        });\n        console.log(\"TOOL RESULTS\", toolResults);\n        if (errors.length > 0) {\n            console.error(\"TOOL ERRORS\", errors);\n            return err(`Tool Errors: ${errors.join(\"\\n\\n\")}`);\n        }\n        // 3) Handle tool results\n        if (toolResults.length > 0) {\n            const summary = await workerWorker({\n                id,\n                description,\n                work: [\n                    ...toolResults.map((toolResult) => toLLMContent(JSON.stringify(toolResult))),\n                ],\n                model,\n                toolManager,\n                summarize: true,\n                chat: false,\n                makeList: disallowNestedMakeList,\n            });\n            if (!ok(summary)) {\n                console.error(\"ERROR FROM SUMMARY\", summary.$error);\n                return summary;\n            }\n            console.log(\"SUMMARY RESPONSE\", summary);\n            const summaryResponse = summary.product;\n            epilog = summary.epilog;\n            return summaryResponse;\n        }\n        return workerResponse;\n    });\n    if (!ok(result))\n        return result;\n    // 4) Handle chat.\n    if (chat) {\n        const last = result.at(-1);\n        epilog ??= \"Please provide feedback on the draft\";\n        await output({\n            schema: {\n                type: \"object\",\n                properties: {\n                    \"a-product\": {\n                        type: \"object\",\n                        behavior: [\"llm-content\"],\n                        title: \"Draft\",\n                    },\n                    \"b-message\": {\n                        type: \"string\",\n                        title: \"\",\n                        format: \"markdown\",\n                    },\n                },\n            },\n            $metadata: {\n                title: \"Writer\",\n                description: \"Asking for feedback on a draft\",\n                icon: \"generative-text\",\n            },\n            \"b-message\": epilog,\n            \"a-product\": last,\n        });\n        const { userInputs } = context;\n        if (!userEndedChat) {\n            const toInput = {\n                type: \"object\",\n                properties: {\n                    request: {\n                        type: \"object\",\n                        title: \"Please provide feedback\",\n                        description: \"Provide feedback or click submit to continue\",\n                        behavior: [\"transient\", \"llm-content\"],\n                        examples: [defaultLLMContent()],\n                    },\n                },\n            };\n            return {\n                toInput,\n                context: {\n                    ...context,\n                    work: result,\n                    last,\n                },\n            };\n        }\n    }\n    // 5) Fall through to default response.\n    return { done: result };\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"object\",\n                    title: \"Agent Context\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                toInput: {\n                    type: \"object\",\n                    title: \"Input Schema\",\n                },\n                context: {\n                    type: \"object\",\n                    title: \"Agent Context\",\n                },\n                done: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Done\",\n                },\n            },\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview The main body of the agent\n */\nimport output from \"@output\";\nimport { ToolManager } from \"./tool-manager\";\nimport workerWorker from \"./worker-worker\";\nimport { report } from \"./output\";\nimport { defaultLLMContent, err, ok } from \"./utils\";\nimport invokeGraph from \"@invoke\";\nimport { Template } from \"./template\";\nimport { ArgumentNameGenerator } from \"./introducer\";\nimport { ListExpander } from \"./lists\";\nexport { invoke as default, describe };\nfunction toLLMContent(text, role = \"user\") {\n    return { parts: [{ text }], role };\n}\nasync function invoke({ context }) {\n    console.log(\"AGENT MAIN\", context);\n    let { id, description, context: initialContext, model, defaultModel, tools, chat, makeList, work: workContext, params, } = context;\n    if (!description) {\n        const $error = \"No instruction supplied\";\n        await report({\n            actor: \"Text Generator\",\n            name: $error,\n            category: \"Runtime error\",\n            details: `In order to run, I need to have an instruction.`,\n        });\n        return { $error };\n    }\n    const template = new Template(description);\n    const toolManager = new ToolManager(new ArgumentNameGenerator());\n    const substituting = await template.substitute(params, async ({ path: url, instance }) => toolManager.addTool(url, instance));\n    if (!ok(substituting)) {\n        return substituting;\n    }\n    description = substituting;\n    if (!(await toolManager.initialize(tools))) {\n        const $error = `Problem initializing tools. \nThe following errors were encountered: ${toolManager.errors.join(\",\")}`;\n        console.error(\"MAIN ERROR\", $error, toolManager.errors);\n        return { $error };\n    }\n    const { userEndedChat, userInputs, last } = context;\n    if (userEndedChat) {\n        if (!last) {\n            return err(\"Chat ended without any work\");\n        }\n        return {\n            done: [...initialContext, last],\n        };\n    }\n    let epilog;\n    const result = await new ListExpander(description, workContext.length > 0 ? workContext : initialContext).map(async (description, work, isList) => {\n        // Disallow making nested lists\n        const disallowNestedMakeList = makeList && !isList;\n        // 1) Make first attempt to make text\n        const response = await workerWorker({\n            id,\n            description,\n            work,\n            model,\n            toolManager,\n            summarize: false,\n            chat,\n            makeList: disallowNestedMakeList,\n        });\n        if (!ok(response)) {\n            console.error(\"ERROR FROM WORKER\", response.$error);\n            return response;\n        }\n        const workerResponse = response.product;\n        epilog = response.epilog;\n        // 2) Call tools\n        const toolResults = [];\n        // TODO: Convert to GeminiPrompt. Somebody. Please.\n        const errors = [];\n        await toolManager.processResponse(workerResponse, async ($board, args) => {\n            const result = await invokeGraph({\n                $board,\n                ...args,\n            });\n            if (\"$error\" in result) {\n                errors.push(result.$error);\n            }\n            toolResults.push(result);\n        });\n        console.log(\"TOOL RESULTS\", toolResults);\n        if (errors.length > 0) {\n            console.error(\"TOOL ERRORS\", errors);\n            return err(`Tool Errors: ${errors.join(\"\\n\\n\")}`);\n        }\n        // 3) Handle tool results\n        if (toolResults.length > 0) {\n            const summary = await workerWorker({\n                id,\n                description,\n                work: [\n                    ...toolResults.map((toolResult) => toLLMContent(JSON.stringify(toolResult))),\n                ],\n                model,\n                toolManager,\n                summarize: true,\n                chat: false,\n                makeList: disallowNestedMakeList,\n            });\n            if (!ok(summary)) {\n                console.error(\"ERROR FROM SUMMARY\", summary.$error);\n                return summary;\n            }\n            console.log(\"SUMMARY RESPONSE\", summary);\n            const summaryResponse = summary.product;\n            epilog = summary.epilog;\n            return summaryResponse;\n        }\n        return workerResponse;\n    });\n    if (!ok(result))\n        return result;\n    // 4) Handle chat.\n    if (chat) {\n        const last = result.at(-1);\n        epilog ?? (epilog = \"Please provide feedback on the draft\");\n        await output({\n            schema: {\n                type: \"object\",\n                properties: {\n                    \"a-product\": {\n                        type: \"object\",\n                        behavior: [\"llm-content\"],\n                        title: \"Draft\",\n                    },\n                    \"b-message\": {\n                        type: \"string\",\n                        title: \"\",\n                        format: \"markdown\",\n                    },\n                },\n            },\n            $metadata: {\n                title: \"Writer\",\n                description: \"Asking for feedback on a draft\",\n                icon: \"generative-text\",\n            },\n            \"b-message\": epilog,\n            \"a-product\": last,\n        });\n        const { userInputs } = context;\n        if (!userEndedChat) {\n            const toInput = {\n                type: \"object\",\n                properties: {\n                    request: {\n                        type: \"object\",\n                        title: \"Please provide feedback\",\n                        description: \"Provide feedback or click submit to continue\",\n                        behavior: [\"transient\", \"llm-content\"],\n                        examples: [defaultLLMContent()],\n                    },\n                },\n            };\n            return {\n                toInput,\n                context: {\n                    ...context,\n                    work: result,\n                    last,\n                },\n            };\n        }\n    }\n    // 5) Fall through to default response.\n    return { done: result };\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"object\",\n                    title: \"Agent Context\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                toInput: {\n                    type: \"object\",\n                    title: \"Input Schema\",\n                },\n                context: {\n                    type: \"object\",\n                    title: \"Agent Context\",\n                },\n                done: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Done\",\n                },\n            },\n        },\n    };\n}\n",
       "metadata": {
         "title": "agent-main",
         "source": {
@@ -171,7 +171,7 @@
       }
     },
     "researcher": {
-      "code": "/**\n * @fileoverview Searching the Internet according to your plan.\n */\nimport { ToolManager } from \"./tool-manager\";\nimport { Template } from \"./template\";\nimport invokeGraph from \"@invoke\";\nimport invokeGemini, { defaultSafetySettings, } from \"./gemini\";\nimport { ok, err, llm, toLLMContent, toText, addUserTurn } from \"./utils\";\nimport { report } from \"./output\";\nimport {} from \"./common\";\nimport { ArgumentNameGenerator } from \"./introducer\";\nexport { invoke as default, describe };\nconst RESEARCH_TOOLS = [\n    {\n        url: \"embed://a2/tools.bgl.json#module:search-web\",\n        title: \"Search Web\",\n    },\n    {\n        url: \"embed://a2/tools.bgl.json#module:search-wikipedia\",\n        title: \"Search Wikipedia\",\n    },\n    {\n        url: \"embed://a2/tools.bgl.json#module:get-webpage\",\n        title: \"Get Webpage\",\n    },\n    {\n        url: \"embed://a2/tools.bgl.json#module:search-maps\",\n        title: \"Search Maps\",\n    },\n];\nconst RESEARCH_MODEL = \"gemini-2.0-flash\";\nconst MAX_ITERATIONS = 7;\nfunction systemInstruction(first) {\n    const which = first ? \"first\" : \"next\";\n    return `You are a researcher.\n  \nYour job is to use the provided research plan to produce raw research that will be later turned into a detailed research report.\nYou are tasked with finding as much of relevant information as possible.\n\nYou examine the conversation context so far and come up with the ${which} step to produce the report, \nusing the conversation context as the the guide of steps taken so far and the outcomes recorded.\n\nYou do not ask user for feedback. You do not try to have a conversation with the user. \nYou know that the user will only ask you to proceed to next step.\n\nYour next step consists of answering two questions.\n\nFirst, ask yourself \"am I done?\" -- looking back at all that you've researched and the plan, \ndo you have enough to produce the detailed report?\n\nSecond, provide a response. Your response must contain two parts:\nThought: a brief plain text reasoning why this is the right ${which} step and a description of what you will do in plain English.\nAction: invoking the tools are your disposal, more than one if necessary. If you're done, do not invoke any tools.`;\n}\nfunction researcherPrompt(contents, plan, tools, first) {\n    return {\n        model: RESEARCH_MODEL,\n        body: {\n            contents: addUserTurn(llm `\nDo the research according to this plan:\n\n---\n\n${plan}\n\n---\n`.asContent(), contents),\n            tools,\n            systemInstruction: toLLMContent(systemInstruction(first)),\n            safetySettings: defaultSafetySettings(),\n        },\n    };\n}\nfunction reportWriterInstruction() {\n    return `You are a research report writer. \nYour teammates produced a wealth of raw research according to the supplied plan.\n\nYour task is to take the raw research and write a thorough, detailed research report that captures it in a way that follows the plan. Use markdown.\n\nA report must additionally contain references to the source (always cite your sources).`;\n}\nfunction reportWriterPrompt(plan, research) {\n    return {\n        model: RESEARCH_MODEL,\n        body: {\n            contents: [toLLMContent(research.join(\"\\n\\n\"))],\n            systemInstruction: toLLMContent(reportWriterInstruction()),\n            safetySettings: defaultSafetySettings(),\n        },\n    };\n}\nasync function thought(response, iteration) {\n    const first = response.parts?.at(0);\n    if (!first || !(\"text\" in first)) {\n        return;\n    }\n    await report({\n        actor: \"Researcher\",\n        category: `Progress report, iteration ${iteration + 1}`,\n        name: \"Thought\",\n        icon: \"generative\",\n        details: first.text\n            .replace(/^Thought: ?/gm, \"\")\n            .replace(/^Action:.*$/gm, \"\")\n            .trim(),\n    });\n}\nasync function invoke({ context, plan, summarize, ...params }) {\n    const tools = RESEARCH_TOOLS.map((descriptor) => descriptor.url);\n    const toolManager = new ToolManager(new ArgumentNameGenerator());\n    let content = context || [toLLMContent(\"Start the research\")];\n    const template = new Template(plan);\n    const substituting = await template.substitute(params, async ({ path: url, instance }) => toolManager.addTool(url, instance));\n    if (!ok(substituting)) {\n        return substituting;\n    }\n    if (!toolManager.hasTools()) {\n        // If no tools supplied (legacy case, actually), initialize\n        // with a set of default tools.\n        const initializing = await toolManager.initialize(tools);\n        if (!initializing) {\n            return err(\"Unable to initialize tools\");\n        }\n    }\n    plan = substituting;\n    const research = [];\n    for (let i = 0; i <= MAX_ITERATIONS; i++) {\n        const askingGemini = await invokeGemini(researcherPrompt(content, plan, toolManager.list(), i === 0));\n        if (!ok(askingGemini)) {\n            return askingGemini;\n        }\n        if (\"context\" in askingGemini) {\n            return err(`Unexpected \"context\" response`);\n        }\n        const response = askingGemini.candidates.at(0)?.content;\n        if (!response) {\n            return err(\"No actionable response\");\n        }\n        await thought(response, i);\n        const toolResponses = [];\n        await toolManager.processResponse(response, async ($board, args) => {\n            toolResponses.push(JSON.stringify(await invokeGraph({ $board, ...args })));\n        });\n        if (toolResponses.length === 0) {\n            break;\n        }\n        research.push(...toolResponses);\n        content = [...content, response, toLLMContent(toolResponses.join(\"\\n\\n\"))];\n    }\n    if (research.length === 0) {\n        await report({\n            actor: \"Researcher\",\n            category: \"Error\",\n            name: \"Error\",\n            details: \"I was unable to obtain any research results\",\n        });\n        return { context };\n    }\n    if (summarize) {\n        const producingReport = await invokeGemini(reportWriterPrompt(plan, research));\n        if (!ok(producingReport)) {\n            return producingReport;\n        }\n        if (\"context\" in producingReport) {\n            return err(`Unexpected \"context\" response`);\n        }\n        const response = producingReport.candidates.at(0)?.content;\n        if (!response) {\n            return err(\"No actionable response\");\n        }\n        return { context: [...(context || []), response] };\n    }\n    return { context: [...(context || []), toLLMContent(research.join(\"\\n\\n\"))] };\n}\nfunction toOxfordList(items) {\n    if (items.length === 0)\n        return \"\";\n    if (items.length === 1)\n        return items[0];\n    if (items.length === 2)\n        return items.join(\" and \");\n    const lastItem = items.pop();\n    return `${items.join(\", \")}, and ${lastItem}`;\n}\nfunction researchExample() {\n    const type = \"tool\";\n    const tools = RESEARCH_TOOLS.map(({ url: path, title }) => Template.part({ title, path, type }));\n    return [\n        JSON.stringify({\n            plan: toLLMContent(`Research the topic provided using ${toOxfordList(tools)} tools`),\n        }),\n    ];\n}\nasync function describe({ inputs: { plan } }) {\n    const template = new Template(plan);\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                plan: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-preview\"],\n                    title: \"Research Plan\",\n                    description: \"Provide an outline of what to research, what areas to cover, etc.\",\n                },\n                summarize: {\n                    type: \"boolean\",\n                    behavior: [\"config\", \"hint-preview\"],\n                    icon: \"summarize\",\n                    title: \"Summarize research\",\n                    description: \"If checked, the Researcher will summarize the results of the research and only pass the research summary along.\",\n                },\n                ...template.schemas(),\n            },\n            behavior: [\"at-wireable\"],\n            ...template.requireds(),\n            additionalProperties: false,\n            examples: researchExample(),\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"main-port\", \"hint-text\"],\n                },\n            },\n            additionalProperties: false,\n        },\n        title: \"Do deep research\",\n        description: \"Do deep research according to your plan\",\n        metadata: {\n            icon: \"generative-search\",\n            tags: [\"quick-access\", \"generative\"],\n            order: 101,\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Searching the Internet according to your plan.\n */\nimport { ToolManager } from \"./tool-manager\";\nimport { Template } from \"./template\";\nimport invokeGraph from \"@invoke\";\nimport invokeGemini, { defaultSafetySettings, } from \"./gemini\";\nimport { ok, err, llm, toLLMContent, addUserTurn } from \"./utils\";\nimport { report } from \"./output\";\nimport { ArgumentNameGenerator } from \"./introducer\";\nexport { invoke as default, describe };\nconst RESEARCH_TOOLS = [\n    {\n        url: \"embed://a2/tools.bgl.json#module:search-web\",\n        title: \"Search Web\",\n    },\n    {\n        url: \"embed://a2/tools.bgl.json#module:search-wikipedia\",\n        title: \"Search Wikipedia\",\n    },\n    {\n        url: \"embed://a2/tools.bgl.json#module:get-webpage\",\n        title: \"Get Webpage\",\n    },\n    {\n        url: \"embed://a2/tools.bgl.json#module:search-maps\",\n        title: \"Search Maps\",\n    },\n];\nconst RESEARCH_MODEL = \"gemini-2.0-flash\";\nconst MAX_ITERATIONS = 7;\nfunction systemInstruction(first) {\n    const which = first ? \"first\" : \"next\";\n    return `You are a researcher.\n  \nYour job is to use the provided research plan to produce raw research that will be later turned into a detailed research report.\nYou are tasked with finding as much of relevant information as possible.\n\nYou examine the conversation context so far and come up with the ${which} step to produce the report, \nusing the conversation context as the the guide of steps taken so far and the outcomes recorded.\n\nYou do not ask user for feedback. You do not try to have a conversation with the user. \nYou know that the user will only ask you to proceed to next step.\n\nYour next step consists of answering two questions.\n\nFirst, ask yourself \"am I done?\" -- looking back at all that you've researched and the plan, \ndo you have enough to produce the detailed report?\n\nSecond, provide a response. Your response must contain two parts:\nThought: a brief plain text reasoning why this is the right ${which} step and a description of what you will do in plain English.\nAction: invoking the tools are your disposal, more than one if necessary. If you're done, do not invoke any tools.`;\n}\nfunction researcherPrompt(contents, plan, tools, first) {\n    return {\n        model: RESEARCH_MODEL,\n        body: {\n            contents: addUserTurn(llm `\nDo the research according to this plan:\n\n---\n\n${plan}\n\n---\n`.asContent(), contents),\n            tools,\n            systemInstruction: toLLMContent(systemInstruction(first)),\n            safetySettings: defaultSafetySettings(),\n        },\n    };\n}\nfunction reportWriterInstruction() {\n    return `You are a research report writer. \nYour teammates produced a wealth of raw research according to the supplied plan.\n\nYour task is to take the raw research and write a thorough, detailed research report that captures it in a way that follows the plan. Use markdown.\n\nA report must additionally contain references to the source (always cite your sources).`;\n}\nfunction reportWriterPrompt(plan, research) {\n    return {\n        model: RESEARCH_MODEL,\n        body: {\n            contents: [toLLMContent(research.join(\"\\n\\n\"))],\n            systemInstruction: toLLMContent(reportWriterInstruction()),\n            safetySettings: defaultSafetySettings(),\n        },\n    };\n}\nasync function thought(response, iteration) {\n    const first = response.parts?.at(0);\n    if (!first || !(\"text\" in first)) {\n        return;\n    }\n    await report({\n        actor: \"Researcher\",\n        category: `Progress report, iteration ${iteration + 1}`,\n        name: \"Thought\",\n        icon: \"generative\",\n        details: first.text\n            .replace(/^Thought: ?/gm, \"\")\n            .replace(/^Action:.*$/gm, \"\")\n            .trim(),\n    });\n}\nasync function invoke({ context, plan, summarize, ...params }) {\n    const tools = RESEARCH_TOOLS.map((descriptor) => descriptor.url);\n    const toolManager = new ToolManager(new ArgumentNameGenerator());\n    let content = context || [toLLMContent(\"Start the research\")];\n    const template = new Template(plan);\n    const substituting = await template.substitute(params, async ({ path: url, instance }) => toolManager.addTool(url, instance));\n    if (!ok(substituting)) {\n        return substituting;\n    }\n    if (!toolManager.hasTools()) {\n        // If no tools supplied (legacy case, actually), initialize\n        // with a set of default tools.\n        const initializing = await toolManager.initialize(tools);\n        if (!initializing) {\n            return err(\"Unable to initialize tools\");\n        }\n    }\n    plan = substituting;\n    const research = [];\n    for (let i = 0; i <= MAX_ITERATIONS; i++) {\n        const askingGemini = await invokeGemini(researcherPrompt(content, plan, toolManager.list(), i === 0));\n        if (!ok(askingGemini)) {\n            return askingGemini;\n        }\n        if (\"context\" in askingGemini) {\n            return err(`Unexpected \"context\" response`);\n        }\n        const response = askingGemini.candidates.at(0)?.content;\n        if (!response) {\n            return err(\"No actionable response\");\n        }\n        await thought(response, i);\n        const toolResponses = [];\n        await toolManager.processResponse(response, async ($board, args) => {\n            toolResponses.push(JSON.stringify(await invokeGraph({ $board, ...args })));\n        });\n        if (toolResponses.length === 0) {\n            break;\n        }\n        research.push(...toolResponses);\n        content = [...content, response, toLLMContent(toolResponses.join(\"\\n\\n\"))];\n    }\n    if (research.length === 0) {\n        await report({\n            actor: \"Researcher\",\n            category: \"Error\",\n            name: \"Error\",\n            details: \"I was unable to obtain any research results\",\n        });\n        return { context };\n    }\n    if (summarize) {\n        const producingReport = await invokeGemini(reportWriterPrompt(plan, research));\n        if (!ok(producingReport)) {\n            return producingReport;\n        }\n        if (\"context\" in producingReport) {\n            return err(`Unexpected \"context\" response`);\n        }\n        const response = producingReport.candidates.at(0)?.content;\n        if (!response) {\n            return err(\"No actionable response\");\n        }\n        return { context: [...(context || []), response] };\n    }\n    return { context: [...(context || []), toLLMContent(research.join(\"\\n\\n\"))] };\n}\nfunction toOxfordList(items) {\n    if (items.length === 0)\n        return \"\";\n    if (items.length === 1)\n        return items[0];\n    if (items.length === 2)\n        return items.join(\" and \");\n    const lastItem = items.pop();\n    return `${items.join(\", \")}, and ${lastItem}`;\n}\nfunction researchExample() {\n    const type = \"tool\";\n    const tools = RESEARCH_TOOLS.map(({ url: path, title }) => Template.part({ title, path, type }));\n    return [\n        JSON.stringify({\n            plan: toLLMContent(`Research the topic provided using ${toOxfordList(tools)} tools`),\n        }),\n    ];\n}\nasync function describe({ inputs: { plan } }) {\n    const template = new Template(plan);\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                plan: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-preview\"],\n                    title: \"Research Plan\",\n                    description: \"Provide an outline of what to research, what areas to cover, etc.\",\n                },\n                summarize: {\n                    type: \"boolean\",\n                    behavior: [\"config\", \"hint-preview\"],\n                    icon: \"summarize\",\n                    title: \"Summarize research\",\n                    description: \"If checked, the Researcher will summarize the results of the research and only pass the research summary along.\",\n                },\n                ...template.schemas(),\n            },\n            behavior: [\"at-wireable\"],\n            ...template.requireds(),\n            additionalProperties: false,\n            examples: researchExample(),\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"main-port\", \"hint-text\"],\n                },\n            },\n            additionalProperties: false,\n        },\n        title: \"Do deep research\",\n        description: \"Do deep research according to your plan\",\n        metadata: {\n            icon: \"generative-search\",\n            tags: [\"quick-access\", \"generative\"],\n            order: 101,\n        },\n    };\n}\n",
       "metadata": {
         "title": "Do deep research",
         "source": {
@@ -183,7 +183,7 @@
       }
     },
     "image-generator": {
-      "code": "/**\n * @fileoverview Generates an image using supplied context (generation only).\n */\nimport invokeBoard from \"@invoke\";\nimport gemini, { defaultSafetySettings, } from \"./gemini\";\nimport { GeminiPrompt } from \"./gemini-prompt\";\nimport { err, ok, toLLMContent, defaultLLMContent, toText, addUserTurn, toTextConcat, joinContent, llm, extractMediaData, extractTextData, mergeContent, } from \"./utils\";\nimport { callImageGen, promptExpander } from \"./image-utils\";\nimport { Template } from \"./template\";\nimport { ToolManager } from \"./tool-manager\";\nimport {} from \"./common\";\nimport { report } from \"./output\";\nimport { ArgumentNameGenerator } from \"./introducer\";\nimport { ListExpander } from \"./lists\";\nconst MAKE_IMAGE_ICON = \"generative-image\";\nconst ASPECT_RATIOS = [\"1:1\", \"9:16\", \"16:9\", \"4:3\", \"3:4\"];\nexport { invoke as default, describe };\nfunction gatheringRequest(contents, instruction, toolManager) {\n    const promptText = llm `\nAnalyze the instruction below and rather than following it, determine what information needs to be gathered to \ngenerate an accurate prompt for a text-to-image model in the next turn:\n-- begin instruction --\n${instruction}\n-- end instruction --\n\nCall the tools to gather the necessary information that could be used to create an accurate prompt.`;\n    return new GeminiPrompt({\n        body: {\n            contents: addUserTurn(promptText.asContent(), contents),\n            tools: toolManager.list(),\n            systemInstruction: toLLMContent(`\nYou are a researcher whose specialty is to call tools whose output helps gather the necessary information\nto be used to create an accurate prompt for a text-to-image model.\n`),\n        },\n    }, toolManager);\n}\nfunction gracefulExit(notOk) {\n    report({\n        actor: \"Make Image\",\n        category: \"Warning\",\n        name: \"Graceful exit\",\n        details: `I tried a couple of times, but the Gemini API failed to generate the image you requested with the following error:\n\n### ${notOk.$error}\n\nTo keep things moving, I will return a blank result. My apologies!`,\n        icon: MAKE_IMAGE_ICON,\n    });\n    return toLLMContent(\" \");\n}\nconst MAX_RETRIES = 5;\nasync function invoke({ context: incomingContext, instruction, \"p-disable-prompt-rewrite\": disablePromptRewrite, \"p-aspect-ratio\": aspectRatio, ...params }) {\n    incomingContext ??= [];\n    if (!instruction) {\n        instruction = toLLMContent(\"\");\n    }\n    if (!aspectRatio) {\n        aspectRatio = \"1:1\";\n    }\n    let imageContext = extractMediaData(incomingContext);\n    const textContext = extractTextData(incomingContext);\n    // Substitute params in instruction.\n    const toolManager = new ToolManager(new ArgumentNameGenerator());\n    const substituting = await new Template(instruction).substitute(params, async ({ path: url, instance }) => toolManager.addTool(url, instance));\n    if (!ok(substituting)) {\n        return substituting;\n    }\n    const fanningOut = await new ListExpander(substituting, incomingContext).map(async (instruction, context) => {\n        // If there are tools in instruction, add an extra step of preparing\n        // information via tools.\n        if (toolManager.hasTools()) {\n            const gatheringInformation = await gatheringRequest(context, instruction, toolManager).invoke();\n            if (!ok(gatheringInformation))\n                return gatheringInformation;\n            context.push(...gatheringInformation.all);\n        }\n        const refImages = extractMediaData([instruction]);\n        const refText = instruction\n            ? toLLMContent(toTextConcat(extractTextData([instruction])))\n            : toLLMContent(\"\");\n        imageContext = imageContext.concat(refImages);\n        let retryCount = MAX_RETRIES;\n        while (retryCount--) {\n            if (imageContext.length > 0) {\n                return err(`References images are not supported with Imagen. For image editing or style transfer, try Gemini Image Generation.`);\n            }\n            else {\n                console.log(\"Step has text only, using generation API\");\n                let imagePrompt;\n                if (disablePromptRewrite) {\n                    imagePrompt = toLLMContent(toText(addUserTurn(refText, context)));\n                }\n                else {\n                    const generatingPrompt = await promptExpander(context, refText).invoke();\n                    if (!ok(generatingPrompt))\n                        return generatingPrompt;\n                    imagePrompt = generatingPrompt.last;\n                }\n                const iPrompt = toText(imagePrompt).trim();\n                console.log(\"PROMPT\", iPrompt);\n                const generatedImage = await callImageGen(iPrompt, aspectRatio);\n                if (!ok(generatedImage))\n                    return generatedImage;\n                return mergeContent(generatedImage, \"model\");\n            }\n        }\n        return gracefulExit(err(`Failed to generate an image after ${MAX_RETRIES} tries.`));\n    });\n    if (!ok(fanningOut))\n        return fanningOut;\n    return { context: fanningOut };\n}\nasync function describe({ inputs: { instruction } }) {\n    const template = new Template(instruction);\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                instruction: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-preview\"],\n                    title: \"Instruction\",\n                    description: \"Describe how to generate the image (content, style, etc). Use @ to reference params or outputs from other steps.\",\n                    default: defaultLLMContent(),\n                },\n                \"p-disable-prompt-rewrite\": {\n                    type: \"boolean\",\n                    title: \"Disable prompt expansion\",\n                    behavior: [\"config\"],\n                    description: \"By default, inputs and instructions will be automatically expanded into a high quality image prompt. Check to disable this re-writing behavior.\",\n                },\n                \"p-aspect-ratio\": {\n                    type: \"string\",\n                    behavior: [\"hint-text\", \"config\"],\n                    title: \"Aspect Ratio\",\n                    enum: ASPECT_RATIOS,\n                    description: \"The aspect ratio of the generated image\",\n                    default: \"1:1\",\n                },\n                ...template.schemas(),\n            },\n            behavior: [\"at-wireable\"],\n            ...template.requireds(),\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"hint-image\", \"main-port\"],\n                },\n            },\n        },\n        title: \"Make Image\",\n        metadata: {\n            icon: MAKE_IMAGE_ICON,\n            tags: [\"quick-access\", \"generative\"],\n            order: 2,\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Generates an image using supplied context (generation only).\n */\nimport { GeminiPrompt } from \"./gemini-prompt\";\nimport { err, ok, toLLMContent, defaultLLMContent, toText, addUserTurn, toTextConcat, llm, extractMediaData, extractTextData, mergeContent, } from \"./utils\";\nimport { callImageGen, promptExpander } from \"./image-utils\";\nimport { Template } from \"./template\";\nimport { ToolManager } from \"./tool-manager\";\nimport { report } from \"./output\";\nimport { ArgumentNameGenerator } from \"./introducer\";\nimport { ListExpander } from \"./lists\";\nconst MAKE_IMAGE_ICON = \"generative-image\";\nconst ASPECT_RATIOS = [\"1:1\", \"9:16\", \"16:9\", \"4:3\", \"3:4\"];\nexport { invoke as default, describe };\nfunction gatheringRequest(contents, instruction, toolManager) {\n    const promptText = llm `\nAnalyze the instruction below and rather than following it, determine what information needs to be gathered to \ngenerate an accurate prompt for a text-to-image model in the next turn:\n-- begin instruction --\n${instruction}\n-- end instruction --\n\nCall the tools to gather the necessary information that could be used to create an accurate prompt.`;\n    return new GeminiPrompt({\n        body: {\n            contents: addUserTurn(promptText.asContent(), contents),\n            tools: toolManager.list(),\n            systemInstruction: toLLMContent(`\nYou are a researcher whose specialty is to call tools whose output helps gather the necessary information\nto be used to create an accurate prompt for a text-to-image model.\n`),\n        },\n    }, toolManager);\n}\nfunction gracefulExit(notOk) {\n    report({\n        actor: \"Make Image\",\n        category: \"Warning\",\n        name: \"Graceful exit\",\n        details: `I tried a couple of times, but the Gemini API failed to generate the image you requested with the following error:\n\n### ${notOk.$error}\n\nTo keep things moving, I will return a blank result. My apologies!`,\n        icon: MAKE_IMAGE_ICON,\n    });\n    return toLLMContent(\" \");\n}\nconst MAX_RETRIES = 5;\nasync function invoke({ context: incomingContext, instruction, \"p-disable-prompt-rewrite\": disablePromptRewrite, \"p-aspect-ratio\": aspectRatio, ...params }) {\n    incomingContext ?? (incomingContext = []);\n    if (!instruction) {\n        instruction = toLLMContent(\"\");\n    }\n    if (!aspectRatio) {\n        aspectRatio = \"1:1\";\n    }\n    let imageContext = extractMediaData(incomingContext);\n    const textContext = extractTextData(incomingContext);\n    // Substitute params in instruction.\n    const toolManager = new ToolManager(new ArgumentNameGenerator());\n    const substituting = await new Template(instruction).substitute(params, async ({ path: url, instance }) => toolManager.addTool(url, instance));\n    if (!ok(substituting)) {\n        return substituting;\n    }\n    const fanningOut = await new ListExpander(substituting, incomingContext).map(async (instruction, context) => {\n        // If there are tools in instruction, add an extra step of preparing\n        // information via tools.\n        if (toolManager.hasTools()) {\n            const gatheringInformation = await gatheringRequest(context, instruction, toolManager).invoke();\n            if (!ok(gatheringInformation))\n                return gatheringInformation;\n            context.push(...gatheringInformation.all);\n        }\n        const refImages = extractMediaData([instruction]);\n        const refText = instruction\n            ? toLLMContent(toTextConcat(extractTextData([instruction])))\n            : toLLMContent(\"\");\n        imageContext = imageContext.concat(refImages);\n        let retryCount = MAX_RETRIES;\n        while (retryCount--) {\n            if (imageContext.length > 0) {\n                return err(`References images are not supported with Imagen. For image editing or style transfer, try Gemini Image Generation.`);\n            }\n            else {\n                console.log(\"Step has text only, using generation API\");\n                let imagePrompt;\n                if (disablePromptRewrite) {\n                    imagePrompt = toLLMContent(toText(addUserTurn(refText, context)));\n                }\n                else {\n                    const generatingPrompt = await promptExpander(context, refText).invoke();\n                    if (!ok(generatingPrompt))\n                        return generatingPrompt;\n                    imagePrompt = generatingPrompt.last;\n                }\n                const iPrompt = toText(imagePrompt).trim();\n                console.log(\"PROMPT\", iPrompt);\n                const generatedImage = await callImageGen(iPrompt, aspectRatio);\n                if (!ok(generatedImage))\n                    return generatedImage;\n                return mergeContent(generatedImage, \"model\");\n            }\n        }\n        return gracefulExit(err(`Failed to generate an image after ${MAX_RETRIES} tries.`));\n    });\n    if (!ok(fanningOut))\n        return fanningOut;\n    return { context: fanningOut };\n}\nasync function describe({ inputs: { instruction } }) {\n    const template = new Template(instruction);\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                instruction: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-preview\"],\n                    title: \"Instruction\",\n                    description: \"Describe how to generate the image (content, style, etc). Use @ to reference params or outputs from other steps.\",\n                    default: defaultLLMContent(),\n                },\n                \"p-disable-prompt-rewrite\": {\n                    type: \"boolean\",\n                    title: \"Disable prompt expansion\",\n                    behavior: [\"config\"],\n                    description: \"By default, inputs and instructions will be automatically expanded into a high quality image prompt. Check to disable this re-writing behavior.\",\n                },\n                \"p-aspect-ratio\": {\n                    type: \"string\",\n                    behavior: [\"hint-text\", \"config\"],\n                    title: \"Aspect Ratio\",\n                    enum: ASPECT_RATIOS,\n                    description: \"The aspect ratio of the generated image\",\n                    default: \"1:1\",\n                },\n                ...template.schemas(),\n            },\n            behavior: [\"at-wireable\"],\n            ...template.requireds(),\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"hint-image\", \"main-port\"],\n                },\n            },\n        },\n        title: \"Make Image\",\n        metadata: {\n            icon: MAKE_IMAGE_ICON,\n            tags: [\"quick-access\", \"generative\"],\n            order: 2,\n        },\n    };\n}\n",
       "metadata": {
         "title": "Make Image",
         "source": {
@@ -195,7 +195,7 @@
       }
     },
     "structured-response": {
-      "code": "import {} from \"./gemini\";\nimport { err, toLLMContent, endsWithRole } from \"./utils\";\nexport { StructuredResponse };\nclass StructuredResponse {\n    id;\n    chat;\n    prolog = \"\";\n    epilog = \"\";\n    body = \"\";\n    response = undefined;\n    constructor(id, chat) {\n        this.id = id;\n        this.chat = chat;\n    }\n    get separator() {\n        return `<sep-${this.id}>`;\n    }\n    addPrompt(c, prompt) {\n        const { parts: p } = prompt;\n        const context = [...c];\n        const parts = [\n            {\n                text: this.instructionText(),\n            },\n            ...p,\n        ];\n        if (endsWithRole(c, \"user\")) {\n            const last = context.pop();\n            context.push({\n                ...last,\n                parts: [...last.parts, ...parts],\n            });\n        }\n        else {\n            context.push({ parts, role: \"user\" });\n        }\n        return context;\n    }\n    instructionText() {\n        const chatOrConclude = this.chat\n            ? `Finally, ask the user to provide feedback on your output as a friendly assistant might.`\n            : `Finally, you briefly summarize what the work product was and how it fulfills the task.`;\n        return `\nConsider the conversation context so far and generate a response.\n\nYour response must consist of three parts, separated by the ${this.separator} tag.\n\n- Briefly describe the work product, why it fulfills the specified task,\nand any notes or comments you might have about it\n- Insert the ${this.separator} tag\n- Provide the work product only, without any additional conversation \nor comments about your output\n- Insert the ${this.separator} tag\n${chatOrConclude}\n`;\n    }\n    instruction() {\n        return toLLMContent(this.instructionText());\n    }\n    parseContent(content) {\n        const part = content.parts?.at(0);\n        if (!part || !(\"text\" in part)) {\n            return err(\"No text in part\");\n        }\n        this.response = content;\n        const structure = part.text.split(this.separator);\n        if (structure.length !== 3) {\n            console.warn(`The output must contain 3 parts, but ${structure.length} were found`);\n            if (structure.length > 1) {\n                // Assume that the prolog and body are here, but the epilog was gone.\n                // This can happen sometimes when we go past the output token window.\n                this.prolog = structure[0];\n                this.body = structure[1].trim();\n                this.epilog = this.chat ? \"Please provide feedback\" : \"\";\n                return;\n            }\n            return err(`No structure response delimiters were found. This is likely an invalid reponse.`);\n        }\n        this.prolog = structure[0];\n        this.body = structure[1].trim();\n        this.epilog = structure[2].trim();\n    }\n    bodyAsContent() {\n        return toLLMContent(this.body, \"model\");\n    }\n    parse(response) {\n        const r = response;\n        const content = r.candidates?.at(0)?.content;\n        if (!content) {\n            return err(\"No content\");\n        }\n        return this.parseContent(content);\n    }\n}\n",
+      "code": "import { err, toLLMContent, endsWithRole } from \"./utils\";\nexport { StructuredResponse };\nclass StructuredResponse {\n    constructor(id, chat) {\n        this.id = id;\n        this.chat = chat;\n        this.prolog = \"\";\n        this.epilog = \"\";\n        this.body = \"\";\n        this.response = undefined;\n    }\n    get separator() {\n        return `<sep-${this.id}>`;\n    }\n    addPrompt(c, prompt) {\n        const { parts: p } = prompt;\n        const context = [...c];\n        const parts = [\n            {\n                text: this.instructionText(),\n            },\n            ...p,\n        ];\n        if (endsWithRole(c, \"user\")) {\n            const last = context.pop();\n            context.push({\n                ...last,\n                parts: [...last.parts, ...parts],\n            });\n        }\n        else {\n            context.push({ parts, role: \"user\" });\n        }\n        return context;\n    }\n    instructionText() {\n        const chatOrConclude = this.chat\n            ? `Finally, ask the user to provide feedback on your output as a friendly assistant might.`\n            : `Finally, you briefly summarize what the work product was and how it fulfills the task.`;\n        return `\nConsider the conversation context so far and generate a response.\n\nYour response must consist of three parts, separated by the ${this.separator} tag.\n\n- Briefly describe the work product, why it fulfills the specified task,\nand any notes or comments you might have about it\n- Insert the ${this.separator} tag\n- Provide the work product only, without any additional conversation \nor comments about your output\n- Insert the ${this.separator} tag\n${chatOrConclude}\n`;\n    }\n    instruction() {\n        return toLLMContent(this.instructionText());\n    }\n    parseContent(content) {\n        const part = content.parts?.at(0);\n        if (!part || !(\"text\" in part)) {\n            return err(\"No text in part\");\n        }\n        this.response = content;\n        const structure = part.text.split(this.separator);\n        if (structure.length !== 3) {\n            console.warn(`The output must contain 3 parts, but ${structure.length} were found`);\n            if (structure.length > 1) {\n                // Assume that the prolog and body are here, but the epilog was gone.\n                // This can happen sometimes when we go past the output token window.\n                this.prolog = structure[0];\n                this.body = structure[1].trim();\n                this.epilog = this.chat ? \"Please provide feedback\" : \"\";\n                return;\n            }\n            return err(`No structure response delimiters were found. This is likely an invalid reponse.`);\n        }\n        this.prolog = structure[0];\n        this.body = structure[1].trim();\n        this.epilog = structure[2].trim();\n    }\n    bodyAsContent() {\n        return toLLMContent(this.body, \"model\");\n    }\n    parse(response) {\n        const r = response;\n        const content = r.candidates?.at(0)?.content;\n        if (!content) {\n            return err(\"No content\");\n        }\n        return this.parseContent(content);\n    }\n}\n",
       "metadata": {
         "title": "structured-response",
         "source": {
@@ -207,7 +207,7 @@
       }
     },
     "template": {
-      "code": "/**\n * @fileoverview Handles templated content\n */\nexport { invoke as default, describe, Template };\nimport {} from \"./common\";\nimport { ok, err, isLLMContent, isLLMContentArray } from \"./utils\";\nimport { ConnectorManager } from \"./connector-manager\";\nimport readFile from \"@read\";\nfunction unique(params) {\n    return Array.from(new Set(params));\n}\nfunction isTool(param) {\n    return param.type === \"tool\" && !!param.path;\n}\nfunction isIn(param) {\n    return param.type === \"in\" && !!param.path;\n}\nfunction isAsset(param) {\n    return param.type === \"asset\" && !!param.path;\n}\nfunction isParameter(param) {\n    return param.type === \"param\" && !!param.path;\n}\nfunction isParamPart(param) {\n    return isTool(param) || isIn(param) || isAsset(param) || isParameter(param);\n}\nconst PARSING_REGEX = /{(?<json>{(?:.*?)})}/gim;\nclass Template {\n    template;\n    #parts;\n    #role;\n    constructor(template) {\n        this.template = template;\n        if (!template) {\n            this.#role = \"user\";\n            this.#parts = [];\n            return;\n        }\n        this.#parts = this.#splitToTemplateParts(template);\n        this.#role = template.role;\n    }\n    #mergeTextParts(parts) {\n        const merged = [];\n        for (let part of parts) {\n            if (\"text\" in part) {\n                const last = merged[merged.length - 1];\n                if (last && \"text\" in last) {\n                    last.text += part.text;\n                }\n                else {\n                    // We do a copy here otherwise the part is mutated, which\n                    // causes problems if the same part appears in the list twice.\n                    part = JSON.parse(JSON.stringify(part));\n                    merged.push(part);\n                }\n            }\n            else {\n                merged.push(part);\n            }\n        }\n        return merged;\n    }\n    /**\n     * Takes an LLM Content and splits it further into parts where\n     * each {{param}} substitution is a separate part.\n     */\n    #splitToTemplateParts(content) {\n        const parts = [];\n        for (const part of content.parts) {\n            if (!(\"text\" in part)) {\n                parts.push(part);\n                continue;\n            }\n            const matches = part.text.matchAll(PARSING_REGEX);\n            let start = 0;\n            for (const match of matches) {\n                const json = match.groups?.json;\n                const op = match.groups?.op;\n                const arg = match.groups?.arg;\n                const end = match.index;\n                if (end > start) {\n                    parts.push({ text: part.text.slice(start, end) });\n                }\n                if (json) {\n                    let maybeTemplatePart;\n                    try {\n                        maybeTemplatePart = JSON.parse(json);\n                        if (isParamPart(maybeTemplatePart)) {\n                            // Do some extra parsing for connector tools\n                            // if (isTool(maybeTemplatePart)) {\n                            //   const [path, connector] = maybeTemplatePart.path.split(\"|\");\n                            //   if (connector && connector.startsWith(\"connectors/\")) {\n                            //     maybeTemplatePart.path = path;\n                            //     maybeTemplatePart.instance = connector;\n                            //   }\n                            //   console.log(\"TOOL\", maybeTemplatePart);\n                            // }\n                            parts.push(maybeTemplatePart);\n                        }\n                        else {\n                            maybeTemplatePart = null;\n                        }\n                    }\n                    catch (e) {\n                        // do nothing\n                    }\n                    finally {\n                        if (!maybeTemplatePart) {\n                            parts.push({ text: part.text.slice(end, end + match[0].length) });\n                        }\n                    }\n                }\n                start = end + match[0].length;\n            }\n            if (start < part.text.length) {\n                parts.push({ text: part.text.slice(start) });\n            }\n        }\n        return parts;\n    }\n    #getLastNonMetadata(value) {\n        const content = value;\n        for (let i = content.length - 1; i >= 0; i--) {\n            if (content[i].role !== \"$metadata\") {\n                return content[i];\n            }\n        }\n        return null;\n    }\n    async #replaceParam(param, params, whenTool) {\n        if (isIn(param)) {\n            const { type, title: name, path } = param;\n            const paramName = `p-z-${path}`;\n            if (paramName in params) {\n                return params[paramName];\n            }\n            return name;\n        }\n        else if (isAsset(param)) {\n            if (ConnectorManager.isConnector(param)) {\n                return new ConnectorManager(param).materialize();\n            }\n            const path = `/assets/${param.path}`;\n            const reading = await readFile({ path });\n            if (!ok(reading)) {\n                return err(`Unable to find asset \"${param.title}\"`);\n            }\n            return reading.data;\n        }\n        else if (isTool(param)) {\n            const substituted = await whenTool(param);\n            if (!ok(substituted))\n                return substituted;\n            return substituted || param.title;\n        }\n        else if (isParameter(param)) {\n            const path = `/env/parameters/${param.path}`;\n            const reading = await readFile({ path });\n            if (!ok(reading)) {\n                console.error(`Unknown parameter \"${param.title}\"`);\n                return null;\n            }\n            return reading.data;\n        }\n        return null;\n    }\n    async substitute(params, whenTool) {\n        const replaced = [];\n        for (const part of this.#parts) {\n            if (\"type\" in part) {\n                const value = await this.#replaceParam(part, params, whenTool);\n                if (value === null) {\n                    // Ignore if null.\n                    continue;\n                }\n                else if (!ok(value)) {\n                    return value;\n                }\n                else if (typeof value === \"string\") {\n                    replaced.push({ text: value });\n                }\n                else if (isLLMContent(value)) {\n                    replaced.push(...value.parts);\n                }\n                else if (isLLMContentArray(value)) {\n                    const last = this.#getLastNonMetadata(value);\n                    if (last) {\n                        replaced.push(...last.parts);\n                    }\n                }\n                else {\n                    replaced.push({ text: JSON.stringify(value) });\n                }\n            }\n            else {\n                replaced.push(part);\n            }\n        }\n        const parts = this.#mergeTextParts(replaced);\n        return { parts, role: this.#role };\n    }\n    #toId(param) {\n        return `p-z-${param}`;\n    }\n    #toTitle(id) {\n        const spaced = id?.replace(/[_-]/g, \" \");\n        return ((spaced?.at(0)?.toUpperCase() ?? \"\") +\n            (spaced?.slice(1)?.toLowerCase() ?? \"\"));\n    }\n    #forEachParam(handler) {\n        for (const part of this.#parts) {\n            if (\"type\" in part) {\n                handler(part);\n            }\n        }\n    }\n    requireds() {\n        const required = [];\n        let hasValues = false;\n        this.#forEachParam((param) => {\n            if (!isIn(param))\n                return;\n            hasValues = true;\n            required.push(this.#toId(param.title));\n        });\n        return hasValues ? { required } : {};\n    }\n    schemas() {\n        const result = [];\n        this.#forEachParam((param) => {\n            const name = param.title;\n            const id = this.#toId(param.path);\n            if (!isIn(param))\n                return;\n            result.push([\n                id,\n                {\n                    title: this.#toTitle(name),\n                    description: `The value to substitute for the parameter \"${name}\"`,\n                    type: \"object\",\n                    behavior: [\"llm-content\"],\n                },\n            ]);\n        });\n        return Object.fromEntries(result);\n    }\n    static part(part) {\n        return `{${JSON.stringify(part)}}`;\n    }\n    /**\n     * This is roughly the same method as `schemas`, but for connectors.\n     * TODO: UNIFY\n     */\n    async schemaProperties() {\n        let result = {};\n        for (const part of this.#parts) {\n            if (!(\"type\" in part))\n                continue;\n            if (!isAsset(part))\n                continue;\n            if (!ConnectorManager.isConnector(part))\n                continue;\n            const props = await new ConnectorManager(part).schemaProperties();\n            result = { ...result, ...props };\n        }\n        return result;\n    }\n    async save(context, options) {\n        if (!context)\n            return;\n        const errors = [];\n        for (const part of this.#parts) {\n            if (!(\"type\" in part))\n                continue;\n            if (!isAsset(part))\n                continue;\n            if (!ConnectorManager.isConnector(part))\n                continue;\n            const saving = await new ConnectorManager(part).save(context, options || {});\n            if (!ok(saving)) {\n                errors.push(saving.$error);\n            }\n        }\n        if (errors.length > 0) {\n            return err(errors.join(\"\\n\"));\n        }\n    }\n}\n/**\n * API for test harness\n */\nfunction fromTestParams(params) {\n    return Object.fromEntries(Object.entries(params).map(([key, value]) => {\n        return [`p-z-${key}`, value];\n    }));\n}\n/**\n * Only used for testing.\n */\nasync function invoke({ inputs: { content, params }, }) {\n    const template = new Template(content);\n    const result = await template.substitute(fromTestParams(params), async (params) => {\n        return params.path;\n    });\n    if (!ok(result)) {\n        return result;\n    }\n    return { outputs: result };\n}\n/**\n * Only used for testing.\n */\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: { inputs: { type: \"object\", title: \"Test inputs\" } },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: { outputs: { type: \"object\", title: \"Test outputs\" } },\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Handles templated content\n */\nvar __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {\n    if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\n    return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\n};\nvar __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n    return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n};\nvar _Template_instances, _Template_parts, _Template_role, _Template_mergeTextParts, _Template_splitToTemplateParts, _Template_getLastNonMetadata, _Template_replaceParam, _Template_toId, _Template_toTitle, _Template_forEachParam;\nexport { invoke as default, describe, Template };\nimport { ok, err, isLLMContent, isLLMContentArray } from \"./utils\";\nimport { ConnectorManager } from \"./connector-manager\";\nimport readFile from \"@read\";\nfunction unique(params) {\n    return Array.from(new Set(params));\n}\nfunction isTool(param) {\n    return param.type === \"tool\" && !!param.path;\n}\nfunction isIn(param) {\n    return param.type === \"in\" && !!param.path;\n}\nfunction isAsset(param) {\n    return param.type === \"asset\" && !!param.path;\n}\nfunction isParameter(param) {\n    return param.type === \"param\" && !!param.path;\n}\nfunction isParamPart(param) {\n    return isTool(param) || isIn(param) || isAsset(param) || isParameter(param);\n}\nconst PARSING_REGEX = /{(?<json>{(?:.*?)})}/gim;\nclass Template {\n    constructor(template) {\n        _Template_instances.add(this);\n        this.template = template;\n        _Template_parts.set(this, void 0);\n        _Template_role.set(this, void 0);\n        if (!template) {\n            __classPrivateFieldSet(this, _Template_role, \"user\", \"f\");\n            __classPrivateFieldSet(this, _Template_parts, [], \"f\");\n            return;\n        }\n        __classPrivateFieldSet(this, _Template_parts, __classPrivateFieldGet(this, _Template_instances, \"m\", _Template_splitToTemplateParts).call(this, template), \"f\");\n        __classPrivateFieldSet(this, _Template_role, template.role, \"f\");\n    }\n    async substitute(params, whenTool) {\n        const replaced = [];\n        for (const part of __classPrivateFieldGet(this, _Template_parts, \"f\")) {\n            if (\"type\" in part) {\n                const value = await __classPrivateFieldGet(this, _Template_instances, \"m\", _Template_replaceParam).call(this, part, params, whenTool);\n                if (value === null) {\n                    // Ignore if null.\n                    continue;\n                }\n                else if (!ok(value)) {\n                    return value;\n                }\n                else if (typeof value === \"string\") {\n                    replaced.push({ text: value });\n                }\n                else if (isLLMContent(value)) {\n                    replaced.push(...value.parts);\n                }\n                else if (isLLMContentArray(value)) {\n                    const last = __classPrivateFieldGet(this, _Template_instances, \"m\", _Template_getLastNonMetadata).call(this, value);\n                    if (last) {\n                        replaced.push(...last.parts);\n                    }\n                }\n                else {\n                    replaced.push({ text: JSON.stringify(value) });\n                }\n            }\n            else {\n                replaced.push(part);\n            }\n        }\n        const parts = __classPrivateFieldGet(this, _Template_instances, \"m\", _Template_mergeTextParts).call(this, replaced);\n        return { parts, role: __classPrivateFieldGet(this, _Template_role, \"f\") };\n    }\n    requireds() {\n        const required = [];\n        let hasValues = false;\n        __classPrivateFieldGet(this, _Template_instances, \"m\", _Template_forEachParam).call(this, (param) => {\n            if (!isIn(param))\n                return;\n            hasValues = true;\n            required.push(__classPrivateFieldGet(this, _Template_instances, \"m\", _Template_toId).call(this, param.title));\n        });\n        return hasValues ? { required } : {};\n    }\n    schemas() {\n        const result = [];\n        __classPrivateFieldGet(this, _Template_instances, \"m\", _Template_forEachParam).call(this, (param) => {\n            const name = param.title;\n            const id = __classPrivateFieldGet(this, _Template_instances, \"m\", _Template_toId).call(this, param.path);\n            if (!isIn(param))\n                return;\n            result.push([\n                id,\n                {\n                    title: __classPrivateFieldGet(this, _Template_instances, \"m\", _Template_toTitle).call(this, name),\n                    description: `The value to substitute for the parameter \"${name}\"`,\n                    type: \"object\",\n                    behavior: [\"llm-content\"],\n                },\n            ]);\n        });\n        return Object.fromEntries(result);\n    }\n    static part(part) {\n        return `{${JSON.stringify(part)}}`;\n    }\n    /**\n     * This is roughly the same method as `schemas`, but for connectors.\n     * TODO: UNIFY\n     */\n    async schemaProperties() {\n        let result = {};\n        for (const part of __classPrivateFieldGet(this, _Template_parts, \"f\")) {\n            if (!(\"type\" in part))\n                continue;\n            if (!isAsset(part))\n                continue;\n            if (!ConnectorManager.isConnector(part))\n                continue;\n            const props = await new ConnectorManager(part).schemaProperties();\n            result = { ...result, ...props };\n        }\n        return result;\n    }\n    async save(context, options) {\n        if (!context)\n            return;\n        const errors = [];\n        for (const part of __classPrivateFieldGet(this, _Template_parts, \"f\")) {\n            if (!(\"type\" in part))\n                continue;\n            if (!isAsset(part))\n                continue;\n            if (!ConnectorManager.isConnector(part))\n                continue;\n            const saving = await new ConnectorManager(part).save(context, options || {});\n            if (!ok(saving)) {\n                errors.push(saving.$error);\n            }\n        }\n        if (errors.length > 0) {\n            return err(errors.join(\"\\n\"));\n        }\n    }\n}\n_Template_parts = new WeakMap(), _Template_role = new WeakMap(), _Template_instances = new WeakSet(), _Template_mergeTextParts = function _Template_mergeTextParts(parts) {\n    const merged = [];\n    for (let part of parts) {\n        if (\"text\" in part) {\n            const last = merged[merged.length - 1];\n            if (last && \"text\" in last) {\n                last.text += part.text;\n            }\n            else {\n                // We do a copy here otherwise the part is mutated, which\n                // causes problems if the same part appears in the list twice.\n                part = JSON.parse(JSON.stringify(part));\n                merged.push(part);\n            }\n        }\n        else {\n            merged.push(part);\n        }\n    }\n    return merged;\n}, _Template_splitToTemplateParts = function _Template_splitToTemplateParts(content) {\n    const parts = [];\n    for (const part of content.parts) {\n        if (!(\"text\" in part)) {\n            parts.push(part);\n            continue;\n        }\n        const matches = part.text.matchAll(PARSING_REGEX);\n        let start = 0;\n        for (const match of matches) {\n            const json = match.groups?.json;\n            const op = match.groups?.op;\n            const arg = match.groups?.arg;\n            const end = match.index;\n            if (end > start) {\n                parts.push({ text: part.text.slice(start, end) });\n            }\n            if (json) {\n                let maybeTemplatePart;\n                try {\n                    maybeTemplatePart = JSON.parse(json);\n                    if (isParamPart(maybeTemplatePart)) {\n                        // Do some extra parsing for connector tools\n                        // if (isTool(maybeTemplatePart)) {\n                        //   const [path, connector] = maybeTemplatePart.path.split(\"|\");\n                        //   if (connector && connector.startsWith(\"connectors/\")) {\n                        //     maybeTemplatePart.path = path;\n                        //     maybeTemplatePart.instance = connector;\n                        //   }\n                        //   console.log(\"TOOL\", maybeTemplatePart);\n                        // }\n                        parts.push(maybeTemplatePart);\n                    }\n                    else {\n                        maybeTemplatePart = null;\n                    }\n                }\n                catch (e) {\n                    // do nothing\n                }\n                finally {\n                    if (!maybeTemplatePart) {\n                        parts.push({ text: part.text.slice(end, end + match[0].length) });\n                    }\n                }\n            }\n            start = end + match[0].length;\n        }\n        if (start < part.text.length) {\n            parts.push({ text: part.text.slice(start) });\n        }\n    }\n    return parts;\n}, _Template_getLastNonMetadata = function _Template_getLastNonMetadata(value) {\n    const content = value;\n    for (let i = content.length - 1; i >= 0; i--) {\n        if (content[i].role !== \"$metadata\") {\n            return content[i];\n        }\n    }\n    return null;\n}, _Template_replaceParam = async function _Template_replaceParam(param, params, whenTool) {\n    if (isIn(param)) {\n        const { type, title: name, path } = param;\n        const paramName = `p-z-${path}`;\n        if (paramName in params) {\n            return params[paramName];\n        }\n        return name;\n    }\n    else if (isAsset(param)) {\n        if (ConnectorManager.isConnector(param)) {\n            return new ConnectorManager(param).materialize();\n        }\n        const path = `/assets/${param.path}`;\n        const reading = await readFile({ path });\n        if (!ok(reading)) {\n            return err(`Unable to find asset \"${param.title}\"`);\n        }\n        return reading.data;\n    }\n    else if (isTool(param)) {\n        const substituted = await whenTool(param);\n        if (!ok(substituted))\n            return substituted;\n        return substituted || param.title;\n    }\n    else if (isParameter(param)) {\n        const path = `/env/parameters/${param.path}`;\n        const reading = await readFile({ path });\n        if (!ok(reading)) {\n            console.error(`Unknown parameter \"${param.title}\"`);\n            return null;\n        }\n        return reading.data;\n    }\n    return null;\n}, _Template_toId = function _Template_toId(param) {\n    return `p-z-${param}`;\n}, _Template_toTitle = function _Template_toTitle(id) {\n    const spaced = id?.replace(/[_-]/g, \" \");\n    return ((spaced?.at(0)?.toUpperCase() ?? \"\") +\n        (spaced?.slice(1)?.toLowerCase() ?? \"\"));\n}, _Template_forEachParam = function _Template_forEachParam(handler) {\n    for (const part of __classPrivateFieldGet(this, _Template_parts, \"f\")) {\n        if (\"type\" in part) {\n            handler(part);\n        }\n    }\n};\n/**\n * API for test harness\n */\nfunction fromTestParams(params) {\n    return Object.fromEntries(Object.entries(params).map(([key, value]) => {\n        return [`p-z-${key}`, value];\n    }));\n}\n/**\n * Only used for testing.\n */\nasync function invoke({ inputs: { content, params }, }) {\n    const template = new Template(content);\n    const result = await template.substitute(fromTestParams(params), async (params) => {\n        return params.path;\n    });\n    if (!ok(result)) {\n        return result;\n    }\n    return { outputs: result };\n}\n/**\n * Only used for testing.\n */\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: { inputs: { type: \"object\", title: \"Test inputs\" } },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: { outputs: { type: \"object\", title: \"Test outputs\" } },\n        },\n    };\n}\n",
       "metadata": {
         "title": "template",
         "source": {
@@ -219,7 +219,7 @@
       }
     },
     "audio-generator": {
-      "code": "/**\n * @fileoverview Generates audio output using supplied context.\n */\nimport gemini, { defaultSafetySettings, } from \"./gemini\";\nimport { err, ok, llm, toLLMContent, toText } from \"./utils\";\nimport {} from \"./common\";\nexport { invoke as default, describe };\nasync function invoke({ context, }) {\n    // 1) Get last LLMContent from input.\n    const prompt = context && Array.isArray(context) && context.length > 0\n        ? context.at(-1)\n        : undefined;\n    if (!prompt) {\n        return err(\"Must supply context as input\");\n    }\n    prompt.role = \"user\";\n    // 2) Call Gemini to generate audio.\n    const result = await gemini({\n        model: \"gemini-2.0-flash-exp\",\n        body: {\n            contents: [prompt],\n            generationConfig: {\n                responseModalities: [\"AUDIO\"],\n            },\n            safetySettings: defaultSafetySettings(),\n        },\n    });\n    if (!ok(result)) {\n        return result;\n    }\n    if (\"context\" in result) {\n        return err(\"Invalid output from Gemini -- must be candidates\");\n    }\n    const content = result.candidates.at(0)?.content;\n    if (!content) {\n        return err(\"No content\");\n    }\n    return { context: [content] };\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n            },\n            additionalProperties: false,\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"hint-audio\"],\n                },\n            },\n            additionalProperties: false,\n        },\n        title: \"Make Audio [Deprecated, Use Make Speech]\",\n        metadata: {\n            icon: \"generative-audio\",\n            tags: [\"quick-access\", \"generative\", \"experimental\"],\n            order: 3,\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Generates audio output using supplied context.\n */\nimport gemini, { defaultSafetySettings, } from \"./gemini\";\nimport { err, ok } from \"./utils\";\nexport { invoke as default, describe };\nasync function invoke({ context, }) {\n    // 1) Get last LLMContent from input.\n    const prompt = context && Array.isArray(context) && context.length > 0\n        ? context.at(-1)\n        : undefined;\n    if (!prompt) {\n        return err(\"Must supply context as input\");\n    }\n    prompt.role = \"user\";\n    // 2) Call Gemini to generate audio.\n    const result = await gemini({\n        model: \"gemini-2.0-flash-exp\",\n        body: {\n            contents: [prompt],\n            generationConfig: {\n                responseModalities: [\"AUDIO\"],\n            },\n            safetySettings: defaultSafetySettings(),\n        },\n    });\n    if (!ok(result)) {\n        return result;\n    }\n    if (\"context\" in result) {\n        return err(\"Invalid output from Gemini -- must be candidates\");\n    }\n    const content = result.candidates.at(0)?.content;\n    if (!content) {\n        return err(\"No content\");\n    }\n    return { context: [content] };\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n            },\n            additionalProperties: false,\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"hint-audio\"],\n                },\n            },\n            additionalProperties: false,\n        },\n        title: \"Make Audio [Deprecated, Use Make Speech]\",\n        metadata: {\n            icon: \"generative-audio\",\n            tags: [\"quick-access\", \"generative\", \"experimental\"],\n            order: 3,\n        },\n    };\n}\n",
       "metadata": {
         "title": "Audio Generator",
         "source": {
@@ -231,7 +231,7 @@
       }
     },
     "text-entry": {
-      "code": "/**\n * @fileoverview Allows asking user for input that could be then used in next steps.\n */\nimport { ok, defaultLLMContent, toText, llm } from \"./utils\";\nimport {} from \"./common\";\nimport { Template } from \"./template\";\nimport { report } from \"./output\";\nexport { invoke as default, describe };\nconst MODALITY = [\n    \"Any\",\n    \"Audio\",\n    \"Image\",\n    \"Text\",\n    \"Upload File\",\n    \"Video\",\n];\nfunction toInput(title, modality) {\n    const toInput = {\n        type: \"object\",\n        properties: {\n            request: {\n                type: \"object\",\n                title,\n                behavior: [\"transient\", \"llm-content\"],\n                examples: [defaultLLMContent()],\n                format: computeIcon(modality),\n            },\n        },\n    };\n    return toInput;\n}\nconst ICONS = {\n    Any: \"asterisk\",\n    Audio: \"mic\",\n    Video: \"videocam\",\n    Image: \"image\",\n    \"Upload File\": \"upload\",\n    Text: \"edit_note\",\n};\nconst HINTS = {\n    Any: \"hint-multimodal\",\n    Audio: \"hint-audio\",\n    Video: \"hint-image\",\n    Image: \"hint-image\",\n    \"Upload File\": \"hint-text\",\n    Text: \"hint-text\",\n};\nfunction computeIcon(modality) {\n    return (modality && ICONS[modality]) || \"asterisk\";\n}\nfunction computeHint(modality) {\n    return (modality && HINTS[modality]) || \"hint-multimodal\";\n}\nfunction combineModalities(modalities) {\n    const schemaEnum = modalities.map((modality) => ({\n        id: modality,\n        title: modality,\n        icon: ICONS[modality],\n    }));\n    return schemaEnum;\n}\nasync function invoke({ description, \"p-modality\": modality, ...params }) {\n    const template = new Template(description);\n    let details = llm `Please provide input`.asContent();\n    if (description) {\n        const substituting = await template.substitute(params, async () => \"\");\n        if (!ok(substituting)) {\n            return substituting;\n        }\n        details = substituting;\n    }\n    await report({\n        actor: \"User Input\",\n        category: \"Requesting Input\",\n        name: \"\",\n        details,\n        icon: \"input\",\n    });\n    const title = toText(details);\n    return { context: \"nothing\", toInput: toInput(title, modality) };\n}\nasync function describe({ inputs: { description, [\"p-modality\"]: modality }, }) {\n    const icon = computeIcon(modality);\n    const template = new Template(description);\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                description: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-preview\"],\n                    title: \"What to ask of user\",\n                    description: \"Provide a request prompt that will be shown to the user.\",\n                },\n                \"p-modality\": {\n                    type: \"string\",\n                    enum: combineModalities(MODALITY),\n                    behavior: [\"config\", \"hint-preview\"],\n                    icon,\n                    title: \"Input type\",\n                    description: \"Set the type of input the user can provide\",\n                },\n                ...template.schemas(),\n            },\n            behavior: [\"at-wireable\"],\n            additionalProperties: true,\n            ...template.requireds(),\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"main-port\", computeHint(modality)],\n                },\n            },\n            additionalProperties: false,\n        },\n        title: \"User Input\",\n        metadata: {\n            icon: \"ask-user\",\n            tags: [\"quick-access\", \"core\", \"input\"],\n            order: 1,\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Allows asking user for input that could be then used in next steps.\n */\nimport { ok, defaultLLMContent, toText, llm } from \"./utils\";\nimport { Template } from \"./template\";\nimport { report } from \"./output\";\nexport { invoke as default, describe };\nconst MODALITY = [\n    \"Any\",\n    \"Audio\",\n    \"Image\",\n    \"Text\",\n    \"Upload File\",\n    \"Video\",\n];\nfunction toInput(title, modality) {\n    const toInput = {\n        type: \"object\",\n        properties: {\n            request: {\n                type: \"object\",\n                title,\n                behavior: [\"transient\", \"llm-content\"],\n                examples: [defaultLLMContent()],\n                format: computeIcon(modality),\n            },\n        },\n    };\n    return toInput;\n}\nconst ICONS = {\n    Any: \"asterisk\",\n    Audio: \"mic\",\n    Video: \"videocam\",\n    Image: \"image\",\n    \"Upload File\": \"upload\",\n    Text: \"edit_note\",\n};\nconst HINTS = {\n    Any: \"hint-multimodal\",\n    Audio: \"hint-audio\",\n    Video: \"hint-image\",\n    Image: \"hint-image\",\n    \"Upload File\": \"hint-text\",\n    Text: \"hint-text\",\n};\nfunction computeIcon(modality) {\n    return (modality && ICONS[modality]) || \"asterisk\";\n}\nfunction computeHint(modality) {\n    return (modality && HINTS[modality]) || \"hint-multimodal\";\n}\nfunction combineModalities(modalities) {\n    const schemaEnum = modalities.map((modality) => ({\n        id: modality,\n        title: modality,\n        icon: ICONS[modality],\n    }));\n    return schemaEnum;\n}\nasync function invoke({ description, \"p-modality\": modality, ...params }) {\n    const template = new Template(description);\n    let details = llm `Please provide input`.asContent();\n    if (description) {\n        const substituting = await template.substitute(params, async () => \"\");\n        if (!ok(substituting)) {\n            return substituting;\n        }\n        details = substituting;\n    }\n    await report({\n        actor: \"User Input\",\n        category: \"Requesting Input\",\n        name: \"\",\n        details,\n        icon: \"input\",\n    });\n    const title = toText(details);\n    return { context: \"nothing\", toInput: toInput(title, modality) };\n}\nasync function describe({ inputs: { description, [\"p-modality\"]: modality }, }) {\n    const icon = computeIcon(modality);\n    const template = new Template(description);\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                description: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-preview\"],\n                    title: \"What to ask of user\",\n                    description: \"Provide a request prompt that will be shown to the user.\",\n                },\n                \"p-modality\": {\n                    type: \"string\",\n                    enum: combineModalities(MODALITY),\n                    behavior: [\"config\", \"hint-preview\"],\n                    icon,\n                    title: \"Input type\",\n                    description: \"Set the type of input the user can provide\",\n                },\n                ...template.schemas(),\n            },\n            behavior: [\"at-wireable\"],\n            additionalProperties: true,\n            ...template.requireds(),\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"main-port\", computeHint(modality)],\n                },\n            },\n            additionalProperties: false,\n        },\n        title: \"User Input\",\n        metadata: {\n            icon: \"ask-user\",\n            tags: [\"quick-access\", \"core\", \"input\"],\n            order: 1,\n        },\n    };\n}\n",
       "metadata": {
         "title": "Ask User",
         "icon": "text",
@@ -268,7 +268,7 @@
       }
     },
     "make-code": {
-      "code": "/**\n * @fileoverview Generates code using supplied context.\n */\nimport invokeBoard from \"@invoke\";\nimport gemini, { defaultSafetySettings, } from \"./gemini\";\nimport { err, ok, toLLMContent, toText, addUserTurn } from \"./utils\";\nimport { Template } from \"./template\";\nimport { ToolManager } from \"./tool-manager\";\nimport {} from \"./common\";\nimport { report } from \"./output\";\nimport { ArgumentNameGenerator } from \"./introducer\";\nconst MAKE_CODE_ICON = \"generative-code\";\nexport { invoke as default, describe };\nfunction gatheringRequest(contents, instruction, language, toolManager) {\n    const promptText = `\nAnalyze the instruction below and rather than following it, determine what information needs to be gathered to \ngenerate an accurate prompt for a text-to-${language} model in the next turn:\n-- begin instruction --\n${toText(instruction)}\n-- end instruction --\n\nCall the tools to gather the necessary information that could be used to create an accurate prompt.`;\n    return new GeminiPrompt({\n        model: \"gemini-1.5-flash-latest\",\n        body: {\n            contents: addUserTurn(promptText, contents),\n            tools: toolManager.list(),\n            systemInstruction: toLLMContent(`\nYou are a researcher whose specialty is to call tools whose output helps gather the necessary information\nto be used to create an accurate prompt for a text-to-${language} model.\n`),\n        },\n    }, toolManager);\n}\nfunction promptRequest(contents, instruction, language) {\n    const context = contents?.length\n        ? \"using conversation context and these additional\"\n        : \"with these\";\n    const promptText = `Generate a single text-to-${language} prompt ${context} instructions:\n${toText(instruction)}\n\nTypical output format:\n\n## Setting/background\n\nDetailed description of everything that is understood about the ${language} code that is being requested.\n\n## Primary focus\n\nDetailed description of the primary functionality or the main focal point of the JavaScript code.\n\n## Style\n\nDetailed description of the style and approach of the code (defensive, TDD, creative, etc.). The output should\nalways an invariably be ${language === \"JavaScript\" ? \"EcmaScript JavaScript Modules\" : language} and be fully functional \nwithout any placeholders. \n\nIf you are dealing with JavaScript you may use imports if and only if the instruction indicates, otherwise you must\ncreate the functionality as a standalone piece of EcmaScript JavaScript.\n\nYou output will be fed directly into the text-to-${language} model, so it must be prompt only, no additional chit-chat\n`;\n    return new GeminiPrompt({\n        model: \"gemini-1.5-flash-latest\",\n        body: {\n            contents: addUserTurn(promptText, contents),\n            systemInstruction: toLLMContent(`\nYou are a world-class ${language} developer whose specialty is to write prompts for text-to-${language} models that \nalways generate valid outputs.\n\nThe prompt must describe every aspect of the functionality in great detail and describe the problem being solved \nin terms of data structures, algorithms, and style. You must use the instruction to fully understand and replicate\nany reference implementations you've been given and you must not augment or deviate from that style. You should\nensure that the prompt includes enough information to fully replicate that style with examples and maximal clarity.\n\nIf the code pertains to user interface work, you must also maximize the accessibility of the code generated with\nappropriate titles and labels for buttons, controls, inputs, etc, and they should never be empty.\n\nBe sure to export all relevant symbols so that the code can be used outside of the EcmaScript Module. Always do\nthis as named symbols rather than using default exports.\n\nIf writing JavaScript, and where a variable is private, use private fields (#field) rather than an underscore at the start.\n`),\n        },\n    });\n}\nfunction codeRequest(prompt, language) {\n    prompt.role = \"user\";\n    prompt.parts.unshift({\n        text: `Generate ${language} code based on this prompt. Output code only, no chit-chat`,\n    });\n    return new GeminiPrompt({\n        model: \"gemini-2.0-flash-exp\",\n        body: {\n            contents: [prompt],\n            generationConfig: {\n                responseModalities: [\"TEXT\"],\n            },\n            safetySettings: defaultSafetySettings(),\n        },\n    });\n}\nclass GeminiPrompt {\n    inputs;\n    toolManager;\n    constructor(inputs, toolManager) {\n        this.inputs = inputs;\n        this.toolManager = toolManager;\n    }\n    async invoke() {\n        const invoking = await gemini(this.inputs);\n        if (!ok(invoking))\n            return invoking;\n        if (\"context\" in invoking) {\n            return err(\"Invalid output from Gemini -- must be candidates\");\n        }\n        const content = invoking.candidates.at(0)?.content;\n        if (!content) {\n            return err(\"No content from Gemini\");\n        }\n        const results = [];\n        const errors = [];\n        await this.toolManager?.processResponse(content, async ($board, args) => {\n            const callingTool = await invokeBoard({ $board, ...args });\n            if (\"$error\" in callingTool) {\n                errors.push(JSON.stringify(callingTool.$error));\n            }\n            else {\n                results.push(JSON.stringify(callingTool));\n            }\n        });\n        if (errors.length) {\n            return err(`Calling tools generated the following errors: ${errors.join(\",\")}`);\n        }\n        const result = [content];\n        if (results.length) {\n            result.push(toLLMContent(results.join(\"\\n\\n\")));\n        }\n        return { all: result, last: result.at(-1) };\n    }\n}\nfunction gracefulExit(notOk) {\n    report({\n        actor: \"Make Code\",\n        category: \"Warning\",\n        name: \"Graceful exit\",\n        details: `I tried a couple of times, but the Gemini API failed to generate the code you requested with the following error:\n\n### ${notOk.$error}\n\nTo keep things moving, I will return a blank result. My apologies!`,\n        icon: MAKE_CODE_ICON,\n    });\n    return { context: [toLLMContent(\" \")] };\n}\nconst MAX_RETRIES = 5;\nasync function invoke({ context, instruction, language, ...params }) {\n    context ??= [];\n    // 1) Substitute params in instruction.\n    const toolManager = new ToolManager(new ArgumentNameGenerator());\n    const substituting = await new Template(instruction).substitute(params, async ({ path: url, instance }) => toolManager.addTool(url, instance));\n    if (!ok(substituting)) {\n        return substituting;\n    }\n    instruction = substituting;\n    // 2) If there are tools in instruction, add an extra step of preparing\n    // information via tools.\n    if (toolManager.hasTools()) {\n        const gatheringInformation = await gatheringRequest(context, instruction, language, toolManager).invoke();\n        if (!ok(gatheringInformation))\n            return gatheringInformation;\n        context.push(...gatheringInformation.all);\n    }\n    let retryCount = MAX_RETRIES;\n    while (retryCount--) {\n        // 3) Call Gemini to generate prompt.\n        const generatingPrompt = await promptRequest(context, instruction, language).invoke();\n        if (!ok(generatingPrompt))\n            return generatingPrompt;\n        console.log(\"PROMPT\", toText(generatingPrompt.last));\n        // 4) Call Gemini to generate image.\n        const generatingCode = await codeRequest(generatingPrompt.last, language).invoke();\n        if (!ok(generatingCode)) {\n            return generatingCode;\n        }\n        return { context: generatingCode.all };\n    }\n    return gracefulExit(err(`Failed to generate ${language} after ${MAX_RETRIES} tries.`));\n}\nasync function describe({ inputs: { instruction } }) {\n    const template = new Template(instruction);\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                instruction: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-preview\"],\n                    title: \"Instruction\",\n                    description: \"Describe how to generate the JavaScript based on the input: focus, functionality, aim of the code\",\n                },\n                language: {\n                    type: \"string\",\n                    behavior: [\"hint-text\", \"config\", \"hint-preview\"],\n                    title: \"Language\",\n                    icon: \"frame-source\",\n                    enum: [\"JavaScript\", \"HTML\", \"CSS\"],\n                    description: \"The language you'd like to generate\",\n                    default: \"JavaScript\",\n                },\n                ...template.schemas(),\n            },\n            ...template.requireds(),\n            additionalProperties: false,\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"hint-code\", \"main-port\"],\n                },\n            },\n            additionalProperties: false,\n        },\n        title: \"Make Code\",\n        metadata: {\n            icon: MAKE_CODE_ICON,\n            tags: [\"quick-access\", \"generative\", \"experimental\"],\n            order: 2,\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Generates code using supplied context.\n */\nimport invokeBoard from \"@invoke\";\nimport gemini, { defaultSafetySettings, } from \"./gemini\";\nimport { err, ok, toLLMContent, toText, addUserTurn } from \"./utils\";\nimport { Template } from \"./template\";\nimport { ToolManager } from \"./tool-manager\";\nimport { report } from \"./output\";\nimport { ArgumentNameGenerator } from \"./introducer\";\nconst MAKE_CODE_ICON = \"generative-code\";\nexport { invoke as default, describe };\nfunction gatheringRequest(contents, instruction, language, toolManager) {\n    const promptText = `\nAnalyze the instruction below and rather than following it, determine what information needs to be gathered to \ngenerate an accurate prompt for a text-to-${language} model in the next turn:\n-- begin instruction --\n${toText(instruction)}\n-- end instruction --\n\nCall the tools to gather the necessary information that could be used to create an accurate prompt.`;\n    return new GeminiPrompt({\n        model: \"gemini-1.5-flash-latest\",\n        body: {\n            contents: addUserTurn(promptText, contents),\n            tools: toolManager.list(),\n            systemInstruction: toLLMContent(`\nYou are a researcher whose specialty is to call tools whose output helps gather the necessary information\nto be used to create an accurate prompt for a text-to-${language} model.\n`),\n        },\n    }, toolManager);\n}\nfunction promptRequest(contents, instruction, language) {\n    const context = contents?.length\n        ? \"using conversation context and these additional\"\n        : \"with these\";\n    const promptText = `Generate a single text-to-${language} prompt ${context} instructions:\n${toText(instruction)}\n\nTypical output format:\n\n## Setting/background\n\nDetailed description of everything that is understood about the ${language} code that is being requested.\n\n## Primary focus\n\nDetailed description of the primary functionality or the main focal point of the JavaScript code.\n\n## Style\n\nDetailed description of the style and approach of the code (defensive, TDD, creative, etc.). The output should\nalways an invariably be ${language === \"JavaScript\" ? \"EcmaScript JavaScript Modules\" : language} and be fully functional \nwithout any placeholders. \n\nIf you are dealing with JavaScript you may use imports if and only if the instruction indicates, otherwise you must\ncreate the functionality as a standalone piece of EcmaScript JavaScript.\n\nYou output will be fed directly into the text-to-${language} model, so it must be prompt only, no additional chit-chat\n`;\n    return new GeminiPrompt({\n        model: \"gemini-1.5-flash-latest\",\n        body: {\n            contents: addUserTurn(promptText, contents),\n            systemInstruction: toLLMContent(`\nYou are a world-class ${language} developer whose specialty is to write prompts for text-to-${language} models that \nalways generate valid outputs.\n\nThe prompt must describe every aspect of the functionality in great detail and describe the problem being solved \nin terms of data structures, algorithms, and style. You must use the instruction to fully understand and replicate\nany reference implementations you've been given and you must not augment or deviate from that style. You should\nensure that the prompt includes enough information to fully replicate that style with examples and maximal clarity.\n\nIf the code pertains to user interface work, you must also maximize the accessibility of the code generated with\nappropriate titles and labels for buttons, controls, inputs, etc, and they should never be empty.\n\nBe sure to export all relevant symbols so that the code can be used outside of the EcmaScript Module. Always do\nthis as named symbols rather than using default exports.\n\nIf writing JavaScript, and where a variable is private, use private fields (#field) rather than an underscore at the start.\n`),\n        },\n    });\n}\nfunction codeRequest(prompt, language) {\n    prompt.role = \"user\";\n    prompt.parts.unshift({\n        text: `Generate ${language} code based on this prompt. Output code only, no chit-chat`,\n    });\n    return new GeminiPrompt({\n        model: \"gemini-2.0-flash-exp\",\n        body: {\n            contents: [prompt],\n            generationConfig: {\n                responseModalities: [\"TEXT\"],\n            },\n            safetySettings: defaultSafetySettings(),\n        },\n    });\n}\nclass GeminiPrompt {\n    constructor(inputs, toolManager) {\n        this.inputs = inputs;\n        this.toolManager = toolManager;\n    }\n    async invoke() {\n        const invoking = await gemini(this.inputs);\n        if (!ok(invoking))\n            return invoking;\n        if (\"context\" in invoking) {\n            return err(\"Invalid output from Gemini -- must be candidates\");\n        }\n        const content = invoking.candidates.at(0)?.content;\n        if (!content) {\n            return err(\"No content from Gemini\");\n        }\n        const results = [];\n        const errors = [];\n        await this.toolManager?.processResponse(content, async ($board, args) => {\n            const callingTool = await invokeBoard({ $board, ...args });\n            if (\"$error\" in callingTool) {\n                errors.push(JSON.stringify(callingTool.$error));\n            }\n            else {\n                results.push(JSON.stringify(callingTool));\n            }\n        });\n        if (errors.length) {\n            return err(`Calling tools generated the following errors: ${errors.join(\",\")}`);\n        }\n        const result = [content];\n        if (results.length) {\n            result.push(toLLMContent(results.join(\"\\n\\n\")));\n        }\n        return { all: result, last: result.at(-1) };\n    }\n}\nfunction gracefulExit(notOk) {\n    report({\n        actor: \"Make Code\",\n        category: \"Warning\",\n        name: \"Graceful exit\",\n        details: `I tried a couple of times, but the Gemini API failed to generate the code you requested with the following error:\n\n### ${notOk.$error}\n\nTo keep things moving, I will return a blank result. My apologies!`,\n        icon: MAKE_CODE_ICON,\n    });\n    return { context: [toLLMContent(\" \")] };\n}\nconst MAX_RETRIES = 5;\nasync function invoke({ context, instruction, language, ...params }) {\n    context ?? (context = []);\n    // 1) Substitute params in instruction.\n    const toolManager = new ToolManager(new ArgumentNameGenerator());\n    const substituting = await new Template(instruction).substitute(params, async ({ path: url, instance }) => toolManager.addTool(url, instance));\n    if (!ok(substituting)) {\n        return substituting;\n    }\n    instruction = substituting;\n    // 2) If there are tools in instruction, add an extra step of preparing\n    // information via tools.\n    if (toolManager.hasTools()) {\n        const gatheringInformation = await gatheringRequest(context, instruction, language, toolManager).invoke();\n        if (!ok(gatheringInformation))\n            return gatheringInformation;\n        context.push(...gatheringInformation.all);\n    }\n    let retryCount = MAX_RETRIES;\n    while (retryCount--) {\n        // 3) Call Gemini to generate prompt.\n        const generatingPrompt = await promptRequest(context, instruction, language).invoke();\n        if (!ok(generatingPrompt))\n            return generatingPrompt;\n        console.log(\"PROMPT\", toText(generatingPrompt.last));\n        // 4) Call Gemini to generate image.\n        const generatingCode = await codeRequest(generatingPrompt.last, language).invoke();\n        if (!ok(generatingCode)) {\n            return generatingCode;\n        }\n        return { context: generatingCode.all };\n    }\n    return gracefulExit(err(`Failed to generate ${language} after ${MAX_RETRIES} tries.`));\n}\nasync function describe({ inputs: { instruction } }) {\n    const template = new Template(instruction);\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                instruction: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-preview\"],\n                    title: \"Instruction\",\n                    description: \"Describe how to generate the JavaScript based on the input: focus, functionality, aim of the code\",\n                },\n                language: {\n                    type: \"string\",\n                    behavior: [\"hint-text\", \"config\", \"hint-preview\"],\n                    title: \"Language\",\n                    icon: \"frame-source\",\n                    enum: [\"JavaScript\", \"HTML\", \"CSS\"],\n                    description: \"The language you'd like to generate\",\n                    default: \"JavaScript\",\n                },\n                ...template.schemas(),\n            },\n            ...template.requireds(),\n            additionalProperties: false,\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"hint-code\", \"main-port\"],\n                },\n            },\n            additionalProperties: false,\n        },\n        title: \"Make Code\",\n        metadata: {\n            icon: MAKE_CODE_ICON,\n            tags: [\"quick-access\", \"generative\", \"experimental\"],\n            order: 2,\n        },\n    };\n}\n",
       "metadata": {
         "title": "Make Code",
         "source": {
@@ -280,7 +280,7 @@
       }
     },
     "gemini-prompt": {
-      "code": "/**\n * @fileoverview Manages Gemini prompt.\n */\nimport invokeBoard from \"@invoke\";\nimport gemini, {} from \"./gemini\";\nimport { ToolManager } from \"./tool-manager\";\nimport { ok, err, toLLMContent, addUserTurn } from \"./utils\";\nexport { GeminiPrompt };\nfunction textToJson(content) {\n    return {\n        ...content,\n        parts: content.parts.map((part) => {\n            if (\"text\" in part) {\n                try {\n                    return { json: JSON.parse(part.text) };\n                }\n                catch (e) {\n                    // fall through.\n                }\n            }\n            return part;\n        }),\n    };\n}\nfunction mergeLastParts(contexts) {\n    const parts = [];\n    for (const context of contexts) {\n        const last = context.at(-1);\n        if (!last)\n            continue;\n        if (!last.parts)\n            continue;\n        parts.push(...last.parts);\n    }\n    return {\n        parts,\n        role: \"user\",\n    };\n}\nclass GeminiPrompt {\n    inputs;\n    options;\n    calledTools = false;\n    // Useful for detecting if subgraphs were invoked, based on whether a tool was invoked with\n    // 'passContext' set to true.\n    calledCustomTools = false;\n    constructor(inputs, options) {\n        this.inputs = inputs;\n        this.options = this.#reconcileOptions(options);\n    }\n    #reconcileOptions(options) {\n        if (!options)\n            return {};\n        if (options instanceof ToolManager) {\n            return { toolManager: options };\n        }\n        return options;\n    }\n    #normalizeArgs(a, passContext) {\n        if (!passContext)\n            return a;\n        const args = a;\n        const context = [...this.inputs.body.contents];\n        const hasContext = \"context\" in args;\n        let contextArg = hasContext\n            ? {}\n            : {\n                context,\n            };\n        return {\n            ...contextArg,\n            ...Object.fromEntries(Object.entries(args).map(([name, value]) => {\n                if (hasContext) {\n                    value = addUserTurn(value, [\n                        ...this.inputs.body.contents,\n                    ]);\n                }\n                return [name, value];\n            })),\n        };\n    }\n    async invoke() {\n        this.calledTools = false;\n        this.calledCustomTools = false;\n        const { allowToolErrors, validator } = this.options;\n        const invoking = await gemini(this.inputs);\n        if (!ok(invoking))\n            return invoking;\n        if (\"context\" in invoking) {\n            return err(\"Invalid output from Gemini -- must be candidates\");\n        }\n        const candidate = invoking.candidates.at(0);\n        const content = candidate?.content;\n        if (!content) {\n            return err(\"No content from Gemini\");\n        }\n        if (!content.parts) {\n            return err(`Gemini failed to generate result due to ${candidate.finishReason}`);\n        }\n        const results = [];\n        const errors = [];\n        if (validator) {\n            const validating = validator(content);\n            if (!ok(validating))\n                return validating;\n        }\n        await this.options.toolManager?.processResponse(content, async ($board, args, passContext, functionName) => {\n            console.log(\"CALLING TOOL\", $board, args, passContext);\n            this.calledTools = true;\n            if (passContext) {\n                // Passing context means we called a subgraph/'custom tool'.\n                this.calledCustomTools = true;\n            }\n            const callingTool = await invokeBoard({\n                $board,\n                ...this.#normalizeArgs(args, passContext),\n            });\n            if (\"$error\" in callingTool) {\n                errors.push(JSON.stringify(callingTool.$error));\n            }\n            else if (functionName === undefined) {\n                errors.push(`No function name for ${JSON.stringify(callingTool)}`);\n            }\n            else {\n                if (passContext) {\n                    if (!(\"context\" in callingTool)) {\n                        errors.push(`No \"context\" port in outputs of \"${$board}\"`);\n                    }\n                    else {\n                        const response = {\n                            [\"value\"]: JSON.stringify(callingTool.context),\n                        };\n                        const responsePart = {\n                            functionResponse: {\n                                name: functionName,\n                                response: response,\n                            },\n                        };\n                        const toolResponseContent = {\n                            role: \"user\",\n                            parts: [responsePart],\n                        };\n                        results.push([toolResponseContent]);\n                        console.log(\"gemini-prompt + passContext, processResponse: \", results);\n                    }\n                }\n                else {\n                    const responsePart = {\n                        functionResponse: {\n                            name: functionName,\n                            response: callingTool,\n                        },\n                    };\n                    const toolResponseContent = {\n                        role: \"user\",\n                        parts: [responsePart],\n                    };\n                    console.log(\"toolResponseContent: \", toolResponseContent);\n                    results.push([toolResponseContent]);\n                    console.log(\"gemini-prompt processResponse: \", results);\n                }\n            }\n        });\n        console.log(\"ERRORS\", errors);\n        if (errors.length && !allowToolErrors) {\n            return err(`Calling tools generated the following errors: ${errors.join(\",\")}`);\n        }\n        const isJSON = this.inputs.body.generationConfig?.responseMimeType == \"application/json\";\n        console.log(\"gemini-prompt before : \", content);\n        const result = isJSON ? [textToJson(content)] : [content];\n        if (results.length) {\n            result.push(mergeLastParts(results));\n        }\n        console.log(\"gemini-prompt pushed mergeLastParts: \", result);\n        return { all: result, last: result.at(-1), candidate };\n    }\n}\n",
+      "code": "/**\n * @fileoverview Manages Gemini prompt.\n */\nvar __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n    return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n};\nvar _GeminiPrompt_instances, _GeminiPrompt_reconcileOptions, _GeminiPrompt_normalizeArgs;\nimport invokeBoard from \"@invoke\";\nimport gemini from \"./gemini\";\nimport { ToolManager } from \"./tool-manager\";\nimport { ok, err, addUserTurn } from \"./utils\";\nexport { GeminiPrompt };\nfunction textToJson(content) {\n    return {\n        ...content,\n        parts: content.parts.map((part) => {\n            if (\"text\" in part) {\n                try {\n                    return { json: JSON.parse(part.text) };\n                }\n                catch (e) {\n                    // fall through.\n                }\n            }\n            return part;\n        }),\n    };\n}\nfunction mergeLastParts(contexts) {\n    const parts = [];\n    for (const context of contexts) {\n        const last = context.at(-1);\n        if (!last)\n            continue;\n        if (!last.parts)\n            continue;\n        parts.push(...last.parts);\n    }\n    return {\n        parts,\n        role: \"user\",\n    };\n}\nclass GeminiPrompt {\n    constructor(inputs, options) {\n        _GeminiPrompt_instances.add(this);\n        this.inputs = inputs;\n        this.calledTools = false;\n        // Useful for detecting if subgraphs were invoked, based on whether a tool was invoked with\n        // 'passContext' set to true.\n        this.calledCustomTools = false;\n        this.options = __classPrivateFieldGet(this, _GeminiPrompt_instances, \"m\", _GeminiPrompt_reconcileOptions).call(this, options);\n    }\n    async invoke() {\n        this.calledTools = false;\n        this.calledCustomTools = false;\n        const { allowToolErrors, validator } = this.options;\n        const invoking = await gemini(this.inputs);\n        if (!ok(invoking))\n            return invoking;\n        if (\"context\" in invoking) {\n            return err(\"Invalid output from Gemini -- must be candidates\");\n        }\n        const candidate = invoking.candidates.at(0);\n        const content = candidate?.content;\n        if (!content) {\n            return err(\"No content from Gemini\");\n        }\n        if (!content.parts) {\n            return err(`Gemini failed to generate result due to ${candidate.finishReason}`);\n        }\n        const results = [];\n        const errors = [];\n        if (validator) {\n            const validating = validator(content);\n            if (!ok(validating))\n                return validating;\n        }\n        await this.options.toolManager?.processResponse(content, async ($board, args, passContext, functionName) => {\n            console.log(\"CALLING TOOL\", $board, args, passContext);\n            this.calledTools = true;\n            if (passContext) {\n                // Passing context means we called a subgraph/'custom tool'.\n                this.calledCustomTools = true;\n            }\n            const callingTool = await invokeBoard({\n                $board,\n                ...__classPrivateFieldGet(this, _GeminiPrompt_instances, \"m\", _GeminiPrompt_normalizeArgs).call(this, args, passContext),\n            });\n            if (\"$error\" in callingTool) {\n                errors.push(JSON.stringify(callingTool.$error));\n            }\n            else if (functionName === undefined) {\n                errors.push(`No function name for ${JSON.stringify(callingTool)}`);\n            }\n            else {\n                if (passContext) {\n                    if (!(\"context\" in callingTool)) {\n                        errors.push(`No \"context\" port in outputs of \"${$board}\"`);\n                    }\n                    else {\n                        const response = {\n                            [\"value\"]: JSON.stringify(callingTool.context),\n                        };\n                        const responsePart = {\n                            functionResponse: {\n                                name: functionName,\n                                response: response,\n                            },\n                        };\n                        const toolResponseContent = {\n                            role: \"user\",\n                            parts: [responsePart],\n                        };\n                        results.push([toolResponseContent]);\n                        console.log(\"gemini-prompt + passContext, processResponse: \", results);\n                    }\n                }\n                else {\n                    const responsePart = {\n                        functionResponse: {\n                            name: functionName,\n                            response: callingTool,\n                        },\n                    };\n                    const toolResponseContent = {\n                        role: \"user\",\n                        parts: [responsePart],\n                    };\n                    console.log(\"toolResponseContent: \", toolResponseContent);\n                    results.push([toolResponseContent]);\n                    console.log(\"gemini-prompt processResponse: \", results);\n                }\n            }\n        });\n        console.log(\"ERRORS\", errors);\n        if (errors.length && !allowToolErrors) {\n            return err(`Calling tools generated the following errors: ${errors.join(\",\")}`);\n        }\n        const isJSON = this.inputs.body.generationConfig?.responseMimeType == \"application/json\";\n        console.log(\"gemini-prompt before : \", content);\n        const result = isJSON ? [textToJson(content)] : [content];\n        if (results.length) {\n            result.push(mergeLastParts(results));\n        }\n        console.log(\"gemini-prompt pushed mergeLastParts: \", result);\n        return { all: result, last: result.at(-1), candidate };\n    }\n}\n_GeminiPrompt_instances = new WeakSet(), _GeminiPrompt_reconcileOptions = function _GeminiPrompt_reconcileOptions(options) {\n    if (!options)\n        return {};\n    if (options instanceof ToolManager) {\n        return { toolManager: options };\n    }\n    return options;\n}, _GeminiPrompt_normalizeArgs = function _GeminiPrompt_normalizeArgs(a, passContext) {\n    if (!passContext)\n        return a;\n    const args = a;\n    const context = [...this.inputs.body.contents];\n    const hasContext = \"context\" in args;\n    let contextArg = hasContext\n        ? {}\n        : {\n            context,\n        };\n    return {\n        ...contextArg,\n        ...Object.fromEntries(Object.entries(args).map(([name, value]) => {\n            if (hasContext) {\n                value = addUserTurn(value, [\n                    ...this.inputs.body.contents,\n                ]);\n            }\n            return [name, value];\n        })),\n    };\n};\n",
       "metadata": {
         "title": "gemini-prompt",
         "source": {
@@ -304,7 +304,7 @@
       }
     },
     "image-editor": {
-      "code": "/**\n * @fileoverview Generates an image using supplied context (generation and editing).\n */\nimport invokeBoard from \"@invoke\";\nimport gemini, { defaultSafetySettings, } from \"./gemini\";\nimport { GeminiPrompt } from \"./gemini-prompt\";\nimport { err, ok, toLLMContent, defaultLLMContent, toText, addUserTurn, toTextConcat, joinContent, llm, extractMediaData, extractTextData, mergeContent, } from \"./utils\";\nimport { callImageGen, callGeminiImage, promptExpander } from \"./image-utils\";\nimport { Template } from \"./template\";\nimport { ToolManager } from \"./tool-manager\";\nimport {} from \"./common\";\nimport { report } from \"./output\";\nimport { ArgumentNameGenerator } from \"./introducer\";\nimport { ListExpander } from \"./lists\";\nconst MAKE_IMAGE_ICON = \"generative-image\";\nconst ASPECT_RATIOS = [\"1:1\", \"9:16\", \"16:9\", \"4:3\", \"3:4\"];\nexport { invoke as default, describe };\nfunction gatheringRequest(contents, instruction, toolManager) {\n    const promptText = llm `\nAnalyze the instruction below and rather than following it, determine what information needs to be gathered to \ngenerate an accurate prompt for a text-to-image model in the next turn:\n-- begin instruction --\n${instruction}\n-- end instruction --\n\nCall the tools to gather the necessary information that could be used to create an accurate prompt.`;\n    return new GeminiPrompt({\n        body: {\n            contents: addUserTurn(promptText.asContent(), contents),\n            tools: toolManager.list(),\n            systemInstruction: toLLMContent(`\nYou are a researcher whose specialty is to call tools whose output helps gather the necessary information\nto be used to create an accurate prompt for a text-to-image model.\n`),\n        },\n    }, toolManager);\n}\nfunction gracefulExit(notOk) {\n    report({\n        actor: \"Make Image\",\n        category: \"Warning\",\n        name: \"Graceful exit\",\n        details: `I tried a couple of times, but the Gemini API failed to generate the image you requested with the following error:\n\n### ${notOk.$error}\n\nTo keep things moving, I will return a blank result. My apologies!`,\n        icon: MAKE_IMAGE_ICON,\n    });\n    return toLLMContent(\" \");\n}\nconst MAX_RETRIES = 5;\nasync function invoke({ context: incomingContext, instruction, \"p-disable-prompt-rewrite\": disablePromptRewrite, \"p-aspect-ratio\": aspectRatio, ...params }) {\n    incomingContext ??= [];\n    if (!instruction) {\n        instruction = toLLMContent(\"\");\n    }\n    if (!aspectRatio) {\n        aspectRatio = \"1:1\";\n    }\n    let imageContext = extractMediaData(incomingContext);\n    const textContext = extractTextData(incomingContext);\n    // Substitute params in instruction.\n    const toolManager = new ToolManager(new ArgumentNameGenerator());\n    const substituting = await new Template(instruction).substitute(params, async ({ path: url, instance }) => toolManager.addTool(url, instance));\n    if (!ok(substituting)) {\n        return substituting;\n    }\n    const fanningOut = await new ListExpander(substituting, incomingContext).map(async (instruction, context) => {\n        // If there are tools in instruction, add an extra step of preparing\n        // information via tools.\n        if (toolManager.hasTools()) {\n            const gatheringInformation = await gatheringRequest(context, instruction, toolManager).invoke();\n            if (!ok(gatheringInformation))\n                return gatheringInformation;\n            context.push(...gatheringInformation.all);\n        }\n        const refImages = extractMediaData([instruction]);\n        const refText = instruction\n            ? toLLMContent(toTextConcat(extractTextData([instruction])))\n            : toLLMContent(\"\");\n        imageContext = imageContext.concat(refImages);\n        let retryCount = MAX_RETRIES;\n        while (retryCount--) {\n            // Image editing case.\n            if (imageContext.length > 0) {\n                console.log(\"Step has reference image, using Gemini Image API: i2i\");\n                const instructionText = refText ? toText(refText) : \"\";\n                const combinedInstruction = toTextConcat(joinContent(instructionText, textContext, false)).trim();\n                if (!combinedInstruction) {\n                    return err(`An image editing instruction must be provided along side the reference image.`);\n                }\n                const finalInstruction = combinedInstruction + \"\\nAspect ratio: \" + aspectRatio;\n                console.log(\"PROMPT: \" + finalInstruction);\n                const generatedImage = await callGeminiImage(finalInstruction, imageContext, disablePromptRewrite, aspectRatio);\n                if (!ok(generatedImage))\n                    return generatedImage;\n                return mergeContent(generatedImage, \"model\");\n            }\n            else {\n                console.log(\"Step has text only, using Gemini Image API: t2i\");\n                const imagePrompt = toLLMContent(toText(addUserTurn(refText, context)));\n                const iPrompt = toText(imagePrompt).trim();\n                console.log(\"PROMPT\", iPrompt);\n                const generatedImage = await callGeminiImage(iPrompt, [], disablePromptRewrite, aspectRatio);\n                if (!ok(generatedImage))\n                    return generatedImage;\n                return mergeContent(generatedImage, \"model\");\n            }\n        }\n        return gracefulExit(err(`Failed to generate an image after ${MAX_RETRIES} tries.`));\n    });\n    if (!ok(fanningOut))\n        return fanningOut;\n    return { context: fanningOut };\n}\nasync function describe({ inputs: { instruction } }) {\n    const template = new Template(instruction);\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                instruction: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-preview\"],\n                    title: \"Instruction\",\n                    description: \"Describe how to generate the image (content, style, etc). Use @ to reference params or outputs from other steps.\",\n                    default: defaultLLMContent(),\n                },\n                \"p-disable-prompt-rewrite\": {\n                    type: \"boolean\",\n                    title: \"Disable prompt expansion\",\n                    behavior: [\"config\"],\n                    description: \"By default, inputs and instructions will be automatically expanded into a high quality image prompt. Check to disable this re-writing behavior.\",\n                },\n                \"p-aspect-ratio\": {\n                    type: \"string\",\n                    behavior: [\"hint-text\", \"config\"],\n                    title: \"Aspect Ratio\",\n                    enum: ASPECT_RATIOS,\n                    description: \"The aspect ratio of the generated image\",\n                    default: \"1:1\",\n                },\n                ...template.schemas(),\n            },\n            behavior: [\"at-wireable\"],\n            ...template.requireds(),\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"hint-image\", \"main-port\"],\n                },\n            },\n        },\n        title: \"Edit Image\",\n        metadata: {\n            icon: MAKE_IMAGE_ICON,\n            tags: [\"quick-access\", \"generative\"],\n            order: 2,\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Generates an image using supplied context (generation and editing).\n */\nimport { GeminiPrompt } from \"./gemini-prompt\";\nimport { err, ok, toLLMContent, defaultLLMContent, toText, addUserTurn, toTextConcat, joinContent, llm, extractMediaData, extractTextData, mergeContent, } from \"./utils\";\nimport { callGeminiImage } from \"./image-utils\";\nimport { Template } from \"./template\";\nimport { ToolManager } from \"./tool-manager\";\nimport { report } from \"./output\";\nimport { ArgumentNameGenerator } from \"./introducer\";\nimport { ListExpander } from \"./lists\";\nconst MAKE_IMAGE_ICON = \"generative-image\";\nconst ASPECT_RATIOS = [\"1:1\", \"9:16\", \"16:9\", \"4:3\", \"3:4\"];\nexport { invoke as default, describe };\nfunction gatheringRequest(contents, instruction, toolManager) {\n    const promptText = llm `\nAnalyze the instruction below and rather than following it, determine what information needs to be gathered to \ngenerate an accurate prompt for a text-to-image model in the next turn:\n-- begin instruction --\n${instruction}\n-- end instruction --\n\nCall the tools to gather the necessary information that could be used to create an accurate prompt.`;\n    return new GeminiPrompt({\n        body: {\n            contents: addUserTurn(promptText.asContent(), contents),\n            tools: toolManager.list(),\n            systemInstruction: toLLMContent(`\nYou are a researcher whose specialty is to call tools whose output helps gather the necessary information\nto be used to create an accurate prompt for a text-to-image model.\n`),\n        },\n    }, toolManager);\n}\nfunction gracefulExit(notOk) {\n    report({\n        actor: \"Make Image\",\n        category: \"Warning\",\n        name: \"Graceful exit\",\n        details: `I tried a couple of times, but the Gemini API failed to generate the image you requested with the following error:\n\n### ${notOk.$error}\n\nTo keep things moving, I will return a blank result. My apologies!`,\n        icon: MAKE_IMAGE_ICON,\n    });\n    return toLLMContent(\" \");\n}\nconst MAX_RETRIES = 5;\nasync function invoke({ context: incomingContext, instruction, \"p-disable-prompt-rewrite\": disablePromptRewrite, \"p-aspect-ratio\": aspectRatio, ...params }) {\n    incomingContext ?? (incomingContext = []);\n    if (!instruction) {\n        instruction = toLLMContent(\"\");\n    }\n    if (!aspectRatio) {\n        aspectRatio = \"1:1\";\n    }\n    let imageContext = extractMediaData(incomingContext);\n    const textContext = extractTextData(incomingContext);\n    // Substitute params in instruction.\n    const toolManager = new ToolManager(new ArgumentNameGenerator());\n    const substituting = await new Template(instruction).substitute(params, async ({ path: url, instance }) => toolManager.addTool(url, instance));\n    if (!ok(substituting)) {\n        return substituting;\n    }\n    const fanningOut = await new ListExpander(substituting, incomingContext).map(async (instruction, context) => {\n        // If there are tools in instruction, add an extra step of preparing\n        // information via tools.\n        if (toolManager.hasTools()) {\n            const gatheringInformation = await gatheringRequest(context, instruction, toolManager).invoke();\n            if (!ok(gatheringInformation))\n                return gatheringInformation;\n            context.push(...gatheringInformation.all);\n        }\n        const refImages = extractMediaData([instruction]);\n        const refText = instruction\n            ? toLLMContent(toTextConcat(extractTextData([instruction])))\n            : toLLMContent(\"\");\n        imageContext = imageContext.concat(refImages);\n        let retryCount = MAX_RETRIES;\n        while (retryCount--) {\n            // Image editing case.\n            if (imageContext.length > 0) {\n                console.log(\"Step has reference image, using Gemini Image API: i2i\");\n                const instructionText = refText ? toText(refText) : \"\";\n                const combinedInstruction = toTextConcat(joinContent(instructionText, textContext, false)).trim();\n                if (!combinedInstruction) {\n                    return err(`An image editing instruction must be provided along side the reference image.`);\n                }\n                const finalInstruction = combinedInstruction + \"\\nAspect ratio: \" + aspectRatio;\n                console.log(\"PROMPT: \" + finalInstruction);\n                const generatedImage = await callGeminiImage(finalInstruction, imageContext, disablePromptRewrite, aspectRatio);\n                if (!ok(generatedImage))\n                    return generatedImage;\n                return mergeContent(generatedImage, \"model\");\n            }\n            else {\n                console.log(\"Step has text only, using Gemini Image API: t2i\");\n                const imagePrompt = toLLMContent(toText(addUserTurn(refText, context)));\n                const iPrompt = toText(imagePrompt).trim();\n                console.log(\"PROMPT\", iPrompt);\n                const generatedImage = await callGeminiImage(iPrompt, [], disablePromptRewrite, aspectRatio);\n                if (!ok(generatedImage))\n                    return generatedImage;\n                return mergeContent(generatedImage, \"model\");\n            }\n        }\n        return gracefulExit(err(`Failed to generate an image after ${MAX_RETRIES} tries.`));\n    });\n    if (!ok(fanningOut))\n        return fanningOut;\n    return { context: fanningOut };\n}\nasync function describe({ inputs: { instruction } }) {\n    const template = new Template(instruction);\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                instruction: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-preview\"],\n                    title: \"Instruction\",\n                    description: \"Describe how to generate the image (content, style, etc). Use @ to reference params or outputs from other steps.\",\n                    default: defaultLLMContent(),\n                },\n                \"p-disable-prompt-rewrite\": {\n                    type: \"boolean\",\n                    title: \"Disable prompt expansion\",\n                    behavior: [\"config\"],\n                    description: \"By default, inputs and instructions will be automatically expanded into a high quality image prompt. Check to disable this re-writing behavior.\",\n                },\n                \"p-aspect-ratio\": {\n                    type: \"string\",\n                    behavior: [\"hint-text\", \"config\"],\n                    title: \"Aspect Ratio\",\n                    enum: ASPECT_RATIOS,\n                    description: \"The aspect ratio of the generated image\",\n                    default: \"1:1\",\n                },\n                ...template.schemas(),\n            },\n            behavior: [\"at-wireable\"],\n            ...template.requireds(),\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"hint-image\", \"main-port\"],\n                },\n            },\n        },\n        title: \"Edit Image\",\n        metadata: {\n            icon: MAKE_IMAGE_ICON,\n            tags: [\"quick-access\", \"generative\"],\n            order: 2,\n        },\n    };\n}\n",
       "metadata": {
         "title": "Image Edit",
         "source": {
@@ -316,7 +316,7 @@
       }
     },
     "introducer": {
-      "code": "/**\n * @fileoverview Handles introduction of the step.\n */\nimport { defaultSafetySettings } from \"./gemini\";\nimport { toLLMContent, toText, ok, err, llm } from \"./utils\";\nimport { ToolManager } from \"./tool-manager\";\nimport { GeminiPrompt } from \"./gemini-prompt\";\nimport {} from \"./common\";\nexport { Introducer, ArgumentNameGenerator };\nfunction introductionSchema() {\n    return {\n        type: \"object\",\n        properties: {\n            title: {\n                type: \"string\",\n                description: \"The title of the agent\",\n            },\n            abilities: {\n                type: \"string\",\n                description: \"Verb-first, third-person summary of the agent's abilities\",\n            },\n            argument: {\n                type: \"string\",\n                description: \"The description of the single text argument that the agent takes as input\",\n            },\n        },\n    };\n}\n/**\n * Attempts to adjust the describer result for subgraphs.\n * Accounts for LLMContent[] `context` property\n * and parameters\n */\nclass ArgumentNameGenerator {\n    #containsContext(describerResult) {\n        if (!describerResult.inputSchema?.properties)\n            return true;\n        const context = describerResult.inputSchema.properties[\"context\"];\n        if (!context)\n            return false;\n        if (context.type === \"array\" && context.items) {\n            return !!context.items.behavior?.includes(\"llm-content\");\n        }\n        return false;\n    }\n    async transform(describerResult) {\n        // If there's no `context` property, exit early.\n        if (!this.#containsContext(describerResult)) {\n            return null;\n        }\n        const { title, description } = describerResult;\n        // Fail transform when there's no title or description.\n        // The resulting function declaration will be a dud anyway.\n        if (!title || !description) {\n            return err(`Custom tool must have a title and a description`);\n        }\n        // Add parameters to the describer.\n        const required = [];\n        const params = Object.fromEntries(Object.entries(describerResult.inputSchema?.properties || {})\n            .filter(([name]) => {\n            if (name === \"context\")\n                return false;\n            required.push(name);\n            return true;\n        })\n            .map(([name, value]) => {\n            return [\n                name,\n                {\n                    ...value,\n                    type: \"string\",\n                },\n            ];\n        }));\n        if (required.length > 0) {\n            return {\n                ...describerResult,\n                inputSchema: {\n                    type: \"object\",\n                    properties: params,\n                    required,\n                },\n            };\n        }\n        // When no parameters found, try to discern the parameter name\n        // from description and title.\n        const naming = await new GeminiPrompt({\n            body: {\n                contents: [this.prompt(describerResult)],\n                safetySettings: defaultSafetySettings(),\n                generationConfig: {\n                    responseSchema: this.schema(),\n                    responseMimeType: \"application/json\",\n                },\n            },\n        }).invoke();\n        if (!ok(naming))\n            return naming;\n        const result = naming.last.parts.at(0).json;\n        return {\n            ...describerResult,\n            inputSchema: {\n                type: \"object\",\n                properties: {\n                    context: {\n                        type: \"string\",\n                        description: result.description,\n                    },\n                },\n            },\n        };\n    }\n    schema() {\n        return {\n            type: \"object\",\n            properties: {\n                description: {\n                    type: \"string\",\n                    description: \"One-sentence description of a function argument\",\n                },\n            },\n        };\n    }\n    prompt(describerResult) {\n        return llm `\nYou are amazing at describing things. Today, you will be coming up a one-sentence description \nof a function argument.\n\nThe function's title is: ${describerResult.title}\n\nThe function's description is ${describerResult.description}\n\nIt takes a single argument.\n\nCome up with a one-sentence description of this argument based on the title/description,\nwith the aim of using this description in a JSON Schema.\n`.asContent();\n    }\n}\nclass Introducer {\n    instruction;\n    toolManager;\n    constructor(instruction, toolManager) {\n        this.instruction = instruction;\n        this.toolManager = toolManager;\n    }\n    prompt() {\n        const tools = this.toolManager?.list() || [];\n        let toolInstruction = \"You have no access to tools of any kind.\";\n        if (tools.length > 0) {\n            toolInstruction = `You have access to the following tools:\n${tools.map((tool) => JSON.stringify(tool)).join(\"\\n\\n\")}\nMake sure to mention your ability to call these particular tools in your abilities.\n`;\n        }\n        return toLLMContent(`You are a project manager for team of AI agents.\nRight now, your job is to summarize what one AI agent does.\nGiven an agent prompt, you distill it to a brief summary of abilities.\nThis summary will be used to decide when to invoke this agent.\n# AI Agent Prompt\n\\`\\`\\`\n${toText(this.instruction)}\n${toolInstruction}\n\\`\\`\\`\nReply in JSON using the provided schema.\n`);\n    }\n    async invoke() {\n        const introducing = await new GeminiPrompt({\n            body: {\n                contents: [this.prompt()],\n                safetySettings: defaultSafetySettings(),\n                generationConfig: {\n                    responseSchema: introductionSchema(),\n                    responseMimeType: \"application/json\",\n                },\n            },\n        }).invoke();\n        if (!ok(introducing))\n            return introducing;\n        const intro = introducing.last.parts.at(0)\n            .json;\n        return {\n            title: intro.title,\n            description: intro.abilities,\n            inputSchema: {\n                type: \"object\",\n                properties: {\n                    context: {\n                        type: \"string\",\n                        description: intro.argument,\n                    },\n                },\n            },\n        };\n    }\n}\n",
+      "code": "/**\n * @fileoverview Handles introduction of the step.\n */\nvar __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n    return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n};\nvar _ArgumentNameGenerator_instances, _ArgumentNameGenerator_containsContext;\nimport { defaultSafetySettings } from \"./gemini\";\nimport { toLLMContent, toText, ok, err, llm } from \"./utils\";\nimport { GeminiPrompt } from \"./gemini-prompt\";\nexport { Introducer, ArgumentNameGenerator };\nfunction introductionSchema() {\n    return {\n        type: \"object\",\n        properties: {\n            title: {\n                type: \"string\",\n                description: \"The title of the agent\",\n            },\n            abilities: {\n                type: \"string\",\n                description: \"Verb-first, third-person summary of the agent's abilities\",\n            },\n            argument: {\n                type: \"string\",\n                description: \"The description of the single text argument that the agent takes as input\",\n            },\n        },\n    };\n}\n/**\n * Attempts to adjust the describer result for subgraphs.\n * Accounts for LLMContent[] `context` property\n * and parameters\n */\nclass ArgumentNameGenerator {\n    constructor() {\n        _ArgumentNameGenerator_instances.add(this);\n    }\n    async transform(describerResult) {\n        // If there's no `context` property, exit early.\n        if (!__classPrivateFieldGet(this, _ArgumentNameGenerator_instances, \"m\", _ArgumentNameGenerator_containsContext).call(this, describerResult)) {\n            return null;\n        }\n        const { title, description } = describerResult;\n        // Fail transform when there's no title or description.\n        // The resulting function declaration will be a dud anyway.\n        if (!title || !description) {\n            return err(`Custom tool must have a title and a description`);\n        }\n        // Add parameters to the describer.\n        const required = [];\n        const params = Object.fromEntries(Object.entries(describerResult.inputSchema?.properties || {})\n            .filter(([name]) => {\n            if (name === \"context\")\n                return false;\n            required.push(name);\n            return true;\n        })\n            .map(([name, value]) => {\n            return [\n                name,\n                {\n                    ...value,\n                    type: \"string\",\n                },\n            ];\n        }));\n        if (required.length > 0) {\n            return {\n                ...describerResult,\n                inputSchema: {\n                    type: \"object\",\n                    properties: params,\n                    required,\n                },\n            };\n        }\n        // When no parameters found, try to discern the parameter name\n        // from description and title.\n        const naming = await new GeminiPrompt({\n            body: {\n                contents: [this.prompt(describerResult)],\n                safetySettings: defaultSafetySettings(),\n                generationConfig: {\n                    responseSchema: this.schema(),\n                    responseMimeType: \"application/json\",\n                },\n            },\n        }).invoke();\n        if (!ok(naming))\n            return naming;\n        const result = naming.last.parts.at(0).json;\n        return {\n            ...describerResult,\n            inputSchema: {\n                type: \"object\",\n                properties: {\n                    context: {\n                        type: \"string\",\n                        description: result.description,\n                    },\n                },\n            },\n        };\n    }\n    schema() {\n        return {\n            type: \"object\",\n            properties: {\n                description: {\n                    type: \"string\",\n                    description: \"One-sentence description of a function argument\",\n                },\n            },\n        };\n    }\n    prompt(describerResult) {\n        return llm `\nYou are amazing at describing things. Today, you will be coming up a one-sentence description \nof a function argument.\n\nThe function's title is: ${describerResult.title}\n\nThe function's description is ${describerResult.description}\n\nIt takes a single argument.\n\nCome up with a one-sentence description of this argument based on the title/description,\nwith the aim of using this description in a JSON Schema.\n`.asContent();\n    }\n}\n_ArgumentNameGenerator_instances = new WeakSet(), _ArgumentNameGenerator_containsContext = function _ArgumentNameGenerator_containsContext(describerResult) {\n    if (!describerResult.inputSchema?.properties)\n        return true;\n    const context = describerResult.inputSchema.properties[\"context\"];\n    if (!context)\n        return false;\n    if (context.type === \"array\" && context.items) {\n        return !!context.items.behavior?.includes(\"llm-content\");\n    }\n    return false;\n};\nclass Introducer {\n    constructor(instruction, toolManager) {\n        this.instruction = instruction;\n        this.toolManager = toolManager;\n    }\n    prompt() {\n        const tools = this.toolManager?.list() || [];\n        let toolInstruction = \"You have no access to tools of any kind.\";\n        if (tools.length > 0) {\n            toolInstruction = `You have access to the following tools:\n${tools.map((tool) => JSON.stringify(tool)).join(\"\\n\\n\")}\nMake sure to mention your ability to call these particular tools in your abilities.\n`;\n        }\n        return toLLMContent(`You are a project manager for team of AI agents.\nRight now, your job is to summarize what one AI agent does.\nGiven an agent prompt, you distill it to a brief summary of abilities.\nThis summary will be used to decide when to invoke this agent.\n# AI Agent Prompt\n\\`\\`\\`\n${toText(this.instruction)}\n${toolInstruction}\n\\`\\`\\`\nReply in JSON using the provided schema.\n`);\n    }\n    async invoke() {\n        const introducing = await new GeminiPrompt({\n            body: {\n                contents: [this.prompt()],\n                safetySettings: defaultSafetySettings(),\n                generationConfig: {\n                    responseSchema: introductionSchema(),\n                    responseMimeType: \"application/json\",\n                },\n            },\n        }).invoke();\n        if (!ok(introducing))\n            return introducing;\n        const intro = introducing.last.parts.at(0)\n            .json;\n        return {\n            title: intro.title,\n            description: intro.abilities,\n            inputSchema: {\n                type: \"object\",\n                properties: {\n                    context: {\n                        type: \"string\",\n                        description: intro.argument,\n                    },\n                },\n            },\n        };\n    }\n}\n",
       "metadata": {
         "title": "introducer",
         "source": {
@@ -328,7 +328,7 @@
       }
     },
     "html-generator": {
-      "code": "/**\n * @fileoverview Utility for calling generate_webpage tool.\n */\nimport fetch from \"@fetch\";\nimport secrets from \"@secrets\";\nimport { err, ok, toLLMContent, toLLMContentInline, toText } from \"./utils\";\nimport { executeStep } from \"./step-executor\";\nimport { report } from \"./output\";\nexport { callGenWebpage };\nconst OUTPUT_KEY = \"rendered_outputs\";\nfunction base64DecodeNonAsciiStandard(base64String) {\n    const byteString = atob(base64String);\n    let encodedString = \"\";\n    for (let i = 0; i < byteString.length; i++) {\n        encodedString +=\n            \"%\" + byteString.charCodeAt(i).toString(16).padStart(2, \"0\");\n    }\n    return decodeURIComponent(encodedString);\n}\nasync function callGenWebpage(instruction, content, renderMode, modelName) {\n    const executionInputs = {};\n    const inputParameters = [];\n    let i = 0;\n    for (let val of content) {\n        for (let part of val.parts) {\n            i++;\n            if (\"text\" in part) {\n                const key = `text_${i}`;\n                inputParameters.push(key);\n                const encodedText = btoa(unescape(encodeURIComponent(part.text)));\n                executionInputs[key] = {\n                    chunks: [\n                        {\n                            mimetype: \"text/plain\",\n                            data: encodedText,\n                        },\n                    ],\n                };\n            }\n            else if (\"inlineData\" in part) {\n                const key = `media_${i}`;\n                inputParameters.push(key);\n                executionInputs[key] = {\n                    chunks: [\n                        {\n                            mimetype: part.inlineData.mimeType,\n                            data: part.inlineData.data,\n                        },\n                    ],\n                };\n            }\n            else if (\"storedData\" in part) {\n                const key = `media_${i}`;\n                inputParameters.push(key);\n                executionInputs[key] = {\n                    chunks: [\n                        {\n                            mimetype: \"url/\" + part.storedData.mimeType,\n                            data: btoa(unescape(encodeURIComponent(part.storedData.handle))),\n                        },\n                    ],\n                };\n            }\n            else {\n                console.error(\"Skipping unexpected content part\");\n            }\n        }\n    }\n    const body = {\n        planStep: {\n            stepName: \"GenerateWebpage\",\n            modelApi: \"generate_webpage\",\n            inputParameters: inputParameters,\n            systemPrompt: \"\",\n            stepIntent: \"\",\n            output: OUTPUT_KEY,\n            options: {\n                disablePromptRewrite: true,\n                renderMode: renderMode,\n                modelName: modelName,\n                systemInstruction: instruction,\n            },\n        },\n        execution_inputs: executionInputs,\n    };\n    // Add the contents\n    // TODO(askerryryan): Remove once functional.\n    console.log(\"request body\");\n    console.log(body);\n    const response = await executeStep(body);\n    // TODO(askerryryan): Remove once functional.\n    console.log(\"response\");\n    console.log(response);\n    if (!ok(response)) {\n        let errorMessage;\n        if (response.$error.includes(\"The service is currently unavailable\")) {\n            errorMessage =\n                \"Request timed out. The model may be experiencing heavy load\";\n        }\n        else {\n            errorMessage = response.$error;\n        }\n        return err(\"Webpage generation failed: \" + errorMessage);\n    }\n    let returnVal;\n    let outputChunk = response.executionOutputs[OUTPUT_KEY];\n    if (!outputChunk) {\n        return err(\"Error: Malformed response. No page generated.\");\n    }\n    const mimetype = outputChunk.chunks[0].mimetype;\n    const base64Data = outputChunk.chunks[0].data;\n    const data = base64DecodeNonAsciiStandard(base64Data);\n    if (mimetype == \"text/html\") {\n        returnVal = toLLMContentInline(mimetype, data);\n    }\n    else {\n        returnVal = toLLMContent(data);\n    }\n    if (!returnVal) {\n        return err(\"Error: No webpage returned from backend\");\n    }\n    return returnVal;\n}\n",
+      "code": "/**\n * @fileoverview Utility for calling generate_webpage tool.\n */\nimport { err, ok, toLLMContent, toLLMContentInline } from \"./utils\";\nimport { executeStep } from \"./step-executor\";\nexport { callGenWebpage };\nconst OUTPUT_KEY = \"rendered_outputs\";\nfunction base64DecodeNonAsciiStandard(base64String) {\n    const byteString = atob(base64String);\n    let encodedString = \"\";\n    for (let i = 0; i < byteString.length; i++) {\n        encodedString +=\n            \"%\" + byteString.charCodeAt(i).toString(16).padStart(2, \"0\");\n    }\n    return decodeURIComponent(encodedString);\n}\nasync function callGenWebpage(instruction, content, renderMode, modelName) {\n    const executionInputs = {};\n    const inputParameters = [];\n    let i = 0;\n    for (let val of content) {\n        for (let part of val.parts) {\n            i++;\n            if (\"text\" in part) {\n                const key = `text_${i}`;\n                inputParameters.push(key);\n                const encodedText = btoa(unescape(encodeURIComponent(part.text)));\n                executionInputs[key] = {\n                    chunks: [\n                        {\n                            mimetype: \"text/plain\",\n                            data: encodedText,\n                        },\n                    ],\n                };\n            }\n            else if (\"inlineData\" in part) {\n                const key = `media_${i}`;\n                inputParameters.push(key);\n                executionInputs[key] = {\n                    chunks: [\n                        {\n                            mimetype: part.inlineData.mimeType,\n                            data: part.inlineData.data,\n                        },\n                    ],\n                };\n            }\n            else if (\"storedData\" in part) {\n                const key = `media_${i}`;\n                inputParameters.push(key);\n                executionInputs[key] = {\n                    chunks: [\n                        {\n                            mimetype: \"url/\" + part.storedData.mimeType,\n                            data: btoa(unescape(encodeURIComponent(part.storedData.handle))),\n                        },\n                    ],\n                };\n            }\n            else {\n                console.error(\"Skipping unexpected content part\");\n            }\n        }\n    }\n    const body = {\n        planStep: {\n            stepName: \"GenerateWebpage\",\n            modelApi: \"generate_webpage\",\n            inputParameters: inputParameters,\n            systemPrompt: \"\",\n            stepIntent: \"\",\n            output: OUTPUT_KEY,\n            options: {\n                disablePromptRewrite: true,\n                renderMode: renderMode,\n                modelName: modelName,\n                systemInstruction: instruction,\n            },\n        },\n        execution_inputs: executionInputs,\n    };\n    // Add the contents\n    // TODO(askerryryan): Remove once functional.\n    console.log(\"request body\");\n    console.log(body);\n    const response = await executeStep(body);\n    // TODO(askerryryan): Remove once functional.\n    console.log(\"response\");\n    console.log(response);\n    if (!ok(response)) {\n        let errorMessage;\n        if (response.$error.includes(\"The service is currently unavailable\")) {\n            errorMessage =\n                \"Request timed out. The model may be experiencing heavy load\";\n        }\n        else {\n            errorMessage = response.$error;\n        }\n        return err(\"Webpage generation failed: \" + errorMessage);\n    }\n    let returnVal;\n    let outputChunk = response.executionOutputs[OUTPUT_KEY];\n    if (!outputChunk) {\n        return err(\"Error: Malformed response. No page generated.\");\n    }\n    const mimetype = outputChunk.chunks[0].mimetype;\n    const base64Data = outputChunk.chunks[0].data;\n    const data = base64DecodeNonAsciiStandard(base64Data);\n    if (mimetype == \"text/html\") {\n        returnVal = toLLMContentInline(mimetype, data);\n    }\n    else {\n        returnVal = toLLMContent(data);\n    }\n    if (!returnVal) {\n        return err(\"Error: No webpage returned from backend\");\n    }\n    return returnVal;\n}\n",
       "metadata": {
         "title": "html-generator",
         "source": {
@@ -340,7 +340,7 @@
       }
     },
     "render-outputs": {
-      "code": "/**\n * @fileoverview Renders multiple outputs into single display.\n */\nimport { Template } from \"./template\";\nimport { ok, err, llm, toText, isEmpty, mergeContent, toLLMContent, } from \"./utils\";\nimport { callGenWebpage } from \"./html-generator\";\nimport { fanOutContext, flattenContext } from \"./lists\";\nimport read from \"@read\";\nexport { invoke as default, describe };\nconst MANUAL_MODE = \"Manual layout\";\nconst AUTO_MODE_LEGACY = \"Webpage with auto-layout\";\nconst FLASH_MODE = \"Webpage with auto-layout by 2.5 Flash\";\nconst PRO_MODE = \"Webpage with auto-layout by 2.5 Pro\";\nfunction defaultSystemInstruction() {\n    return llm `You are a skilled web developer specializing in creating intuitive and visually appealing HTML web pages based on user instructions and data. Your task is to generate a valid HTML webpage that will be rendered in an iframe. The generated code must be valid and functional HTML with JavaScript and CSS embedded inline within <script> and <style> tags respectively. Return only the code, and open the HTML codeblock with the literal string '\\`\\`\\`html'. Render content as a clean, well-structured webpage, paying careful attention to user instructions. Use a responsive or mobile-friendly layout whenever possible and minimize unnecessary padding or margins.`.asContent();\n}\nfunction defaultThemeColors() {\n    return {\n        primaryColor: \"#246db5\",\n        secondaryColor: \"#5cadff\",\n        backgroundColor: \"#ffffff\",\n        textColor: \"#1a1a1a\",\n        primaryTextColor: \"#ffffff\",\n    };\n}\nasync function getThemeColors() {\n    const readingMetadata = await read({ path: \"/env/metadata\" });\n    if (!ok(readingMetadata))\n        return defaultThemeColors();\n    const metadata = readingMetadata.data?.at(0)?.parts?.at(0)\n        ?.json;\n    if (!metadata)\n        return defaultThemeColors();\n    const currentThemeId = metadata?.visual?.presentation?.theme;\n    if (!currentThemeId)\n        return defaultThemeColors();\n    const themeColors = metadata?.visual?.presentation?.themes?.[currentThemeId]?.themeColors;\n    if (!themeColors)\n        return defaultThemeColors();\n    return { ...defaultThemeColors(), ...themeColors };\n}\nfunction themeColorsPrompt(colors) {\n    return `Unless otherwise specified, use the following theme colors:\n\n- primary color: ${colors.primaryColor}\n- secondary color: ${colors.secondaryColor}\n- background color: ${colors.backgroundColor}\n- text color: ${colors.textColor}\n- primary text color: ${colors.primaryTextColor}\n\n`;\n}\nasync function invoke({ text, \"p-render-mode\": renderMode, \"b-system-instruction\": systemInstruction, ...params }) {\n    if (!text) {\n        text = toLLMContent(\"\");\n    }\n    if (!systemInstruction) {\n        systemInstruction = defaultSystemInstruction();\n    }\n    let systemText = toText(systemInstruction);\n    const template = new Template(text);\n    const substituting = await template.substitute(params, async () => \"\");\n    if (!ok(substituting)) {\n        return substituting;\n    }\n    const context = mergeContent(flattenContext([substituting], true, \"\\n\\n\"), \"user\");\n    let modelName = \"\";\n    // TODO(askerryryan): Clean up after backend backwards compatibility window.\n    if (renderMode == MANUAL_MODE) {\n        renderMode = \"Manual\";\n    }\n    else if (renderMode == FLASH_MODE || renderMode == AUTO_MODE_LEGACY) {\n        modelName = \"gemini-2.5-flash-preview-04-17\";\n        renderMode = \"HTML\";\n    }\n    else if (renderMode == PRO_MODE) {\n        modelName = \"gemini-2.5-pro-preview-05-06\";\n        renderMode = \"Interactive\";\n    }\n    else if (!renderMode) {\n        renderMode = \"Manual\";\n    }\n    console.log(\"Rendering mode: \" + renderMode);\n    let out = context;\n    if (renderMode != \"Manual\") {\n        systemText += themeColorsPrompt(await getThemeColors());\n        const webPage = await callGenWebpage(systemText, [context], renderMode, modelName);\n        if (!ok(webPage)) {\n            console.error(\"Failed to generated html output\");\n            return webPage;\n        }\n        else {\n            out = await webPage;\n            console.log(out);\n        }\n    }\n    if (!ok(out))\n        return err(out);\n    return { context: out };\n}\nasync function describe({ inputs: { text } }) {\n    const template = new Template(text);\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                text: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"hint-preview\", \"config\", \"at-wireable\"],\n                    title: \"Outputs to render\",\n                    description: \"Type the @ character to select the outputs to combine. Optionally include style and layout guidlines if using Rendering mode of Markdown or HTML.\",\n                },\n                \"p-render-mode\": {\n                    type: \"string\",\n                    enum: [MANUAL_MODE, FLASH_MODE, PRO_MODE],\n                    title: \"Display format\",\n                    behavior: [\"config\", \"hint-preview\"],\n                    default: MANUAL_MODE,\n                    description: \"Choose how to combine and display the outputs\",\n                },\n                \"b-system-instruction\": {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-advanced\"],\n                    title: \"System Instruction\",\n                    description: \"The system instruction used for auto-layout\",\n                },\n                ...template.schemas(),\n            },\n            behavior: [\"at-wireable\"],\n            ...template.requireds(),\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: {\n                        type: \"object\",\n                        behavior: [\"llm-content\"],\n                    },\n                    title: \"Context out\",\n                    behavior: [\"main-port\", \"hint-multimodal\"],\n                },\n            },\n        },\n        title: \"Display\",\n        metadata: {\n            icon: \"display\",\n            tags: [\"quick-access\", \"core\", \"output\"],\n            order: 100,\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Renders multiple outputs into single display.\n */\nimport { Template } from \"./template\";\nimport { ok, err, llm, toText, mergeContent, toLLMContent, } from \"./utils\";\nimport { callGenWebpage } from \"./html-generator\";\nimport { flattenContext } from \"./lists\";\nimport read from \"@read\";\nexport { invoke as default, describe };\nconst MANUAL_MODE = \"Manual layout\";\nconst AUTO_MODE_LEGACY = \"Webpage with auto-layout\";\nconst FLASH_MODE = \"Webpage with auto-layout by 2.5 Flash\";\nconst PRO_MODE = \"Webpage with auto-layout by 2.5 Pro\";\nfunction defaultSystemInstruction() {\n    return llm `You are a skilled web developer specializing in creating intuitive and visually appealing HTML web pages based on user instructions and data. Your task is to generate a valid HTML webpage that will be rendered in an iframe. The generated code must be valid and functional HTML with JavaScript and CSS embedded inline within <script> and <style> tags respectively. Return only the code, and open the HTML codeblock with the literal string '\\`\\`\\`html'. Render content as a clean, well-structured webpage, paying careful attention to user instructions. Use a responsive or mobile-friendly layout whenever possible and minimize unnecessary padding or margins.`.asContent();\n}\nfunction defaultThemeColors() {\n    return {\n        primaryColor: \"#246db5\",\n        secondaryColor: \"#5cadff\",\n        backgroundColor: \"#ffffff\",\n        textColor: \"#1a1a1a\",\n        primaryTextColor: \"#ffffff\",\n    };\n}\nasync function getThemeColors() {\n    const readingMetadata = await read({ path: \"/env/metadata\" });\n    if (!ok(readingMetadata))\n        return defaultThemeColors();\n    const metadata = readingMetadata.data?.at(0)?.parts?.at(0)\n        ?.json;\n    if (!metadata)\n        return defaultThemeColors();\n    const currentThemeId = metadata?.visual?.presentation?.theme;\n    if (!currentThemeId)\n        return defaultThemeColors();\n    const themeColors = metadata?.visual?.presentation?.themes?.[currentThemeId]?.themeColors;\n    if (!themeColors)\n        return defaultThemeColors();\n    return { ...defaultThemeColors(), ...themeColors };\n}\nfunction themeColorsPrompt(colors) {\n    return `Unless otherwise specified, use the following theme colors:\n\n- primary color: ${colors.primaryColor}\n- secondary color: ${colors.secondaryColor}\n- background color: ${colors.backgroundColor}\n- text color: ${colors.textColor}\n- primary text color: ${colors.primaryTextColor}\n\n`;\n}\nasync function invoke({ text, \"p-render-mode\": renderMode, \"b-system-instruction\": systemInstruction, ...params }) {\n    if (!text) {\n        text = toLLMContent(\"\");\n    }\n    if (!systemInstruction) {\n        systemInstruction = defaultSystemInstruction();\n    }\n    let systemText = toText(systemInstruction);\n    const template = new Template(text);\n    const substituting = await template.substitute(params, async () => \"\");\n    if (!ok(substituting)) {\n        return substituting;\n    }\n    const context = mergeContent(flattenContext([substituting], true, \"\\n\\n\"), \"user\");\n    let modelName = \"\";\n    // TODO(askerryryan): Clean up after backend backwards compatibility window.\n    if (renderMode == MANUAL_MODE) {\n        renderMode = \"Manual\";\n    }\n    else if (renderMode == FLASH_MODE || renderMode == AUTO_MODE_LEGACY) {\n        modelName = \"gemini-2.5-flash-preview-04-17\";\n        renderMode = \"HTML\";\n    }\n    else if (renderMode == PRO_MODE) {\n        modelName = \"gemini-2.5-pro-preview-05-06\";\n        renderMode = \"Interactive\";\n    }\n    else if (!renderMode) {\n        renderMode = \"Manual\";\n    }\n    console.log(\"Rendering mode: \" + renderMode);\n    let out = context;\n    if (renderMode != \"Manual\") {\n        systemText += themeColorsPrompt(await getThemeColors());\n        const webPage = await callGenWebpage(systemText, [context], renderMode, modelName);\n        if (!ok(webPage)) {\n            console.error(\"Failed to generated html output\");\n            return webPage;\n        }\n        else {\n            out = await webPage;\n            console.log(out);\n        }\n    }\n    if (!ok(out))\n        return err(out);\n    return { context: out };\n}\nasync function describe({ inputs: { text } }) {\n    const template = new Template(text);\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                text: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"hint-preview\", \"config\", \"at-wireable\"],\n                    title: \"Outputs to render\",\n                    description: \"Type the @ character to select the outputs to combine. Optionally include style and layout guidlines if using Rendering mode of Markdown or HTML.\",\n                },\n                \"p-render-mode\": {\n                    type: \"string\",\n                    enum: [MANUAL_MODE, FLASH_MODE, PRO_MODE],\n                    title: \"Display format\",\n                    behavior: [\"config\", \"hint-preview\"],\n                    default: MANUAL_MODE,\n                    description: \"Choose how to combine and display the outputs\",\n                },\n                \"b-system-instruction\": {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-advanced\"],\n                    title: \"System Instruction\",\n                    description: \"The system instruction used for auto-layout\",\n                },\n                ...template.schemas(),\n            },\n            behavior: [\"at-wireable\"],\n            ...template.requireds(),\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: {\n                        type: \"object\",\n                        behavior: [\"llm-content\"],\n                    },\n                    title: \"Context out\",\n                    behavior: [\"main-port\", \"hint-multimodal\"],\n                },\n            },\n        },\n        title: \"Display\",\n        metadata: {\n            icon: \"display\",\n            tags: [\"quick-access\", \"core\", \"output\"],\n            order: 100,\n        },\n    };\n}\n",
       "metadata": {
         "title": "render-outputs",
         "source": {
@@ -352,7 +352,7 @@
       }
     },
     "lists": {
-      "code": "/**\n * @fileoverview Handles lists according to https://github.com/breadboard-ai/breadboard/wiki/Step-Listification\n */\nimport { ok, err, generateId, mergeTextParts, toLLMContent, llm, } from \"./utils\";\nimport {} from \"./gemini\";\nexport { fanOutContext, flattenContext, hasLists, addContent, toList, listPrompt, listSchema, ListExpander, };\nfunction isListPart(o) {\n    return !!o && \"list\" in o;\n}\nfunction emptyContent() {\n    return { parts: [{ text: \"\" }] };\n}\nfunction addContent(context, content) {\n    const last = context.at(-1);\n    const maybeList = last?.parts?.at(0);\n    const remainder = context.slice(0, -1);\n    if (isListPart(maybeList)) {\n        const list = maybeList.list.map((item) => ({\n            ...item,\n            content: [...item.content, content],\n        }));\n        return [...remainder, { ...last, parts: [{ ...maybeList, list }] }];\n    }\n    return [...context, content];\n}\nfunction unzipContent(content) {\n    // 1) Scan content for lists.\n    const info = new Set();\n    const ids = new Set();\n    let maxLength = 0;\n    content.parts.forEach((part, index) => {\n        if (!isListPart(part))\n            return;\n        const length = part.list.length;\n        if (length > maxLength)\n            maxLength = length;\n        info.add(index);\n        ids.add(part.id);\n    });\n    if (ids.size > 1) {\n        console.warn(\"Multiple list sources aren't yet supported in instruction, using the first one\");\n    }\n    const id = [...ids].at(0) || \"\";\n    if (info.size === 0) {\n        return { contents: [content], id };\n    }\n    // 2) Create a list and replace lists with list entries.\n    return {\n        id,\n        contents: new Array(maxLength).fill(0).map((_, entryIndex) => {\n            const parts = mergeTextParts(content.parts.flatMap((part, partIndex) => {\n                if (!info.has(partIndex))\n                    return part;\n                // We know this exists, so we're ok with not checking\n                // for existence.\n                const item = part.list.at(entryIndex);\n                if (!item) {\n                    return [];\n                }\n                const last = item.content.at(-1);\n                return last ? last.parts : [];\n            }));\n            return { ...content, parts };\n        }),\n    };\n}\nfunction hasLists(context) {\n    return isListPart(context.at(-1)?.parts?.at(0));\n}\n// TODO: When back to implementing nested lists, this approach\n// is wrong. Instead of expanding items and then converging them,\n// we need to iterate over existing structure. Because then, when\n// it's time to converge, we don't need to try to reconstruct\n// the original structure.\n// Also, it is unclear what we should do with nested lists\n// in instruction. It feels like a similar story, and now\n// we need to support multiple lists and hierarchies in\n// instruction.\nclass ListExpander {\n    instruction;\n    context;\n    prolog;\n    // Local prolog -- the parts of context that were preceding the list.\n    #prolog = [];\n    #list = [];\n    #originalListItems = [];\n    #id = \"\";\n    #instructions;\n    #expanded = false;\n    constructor(instruction, context, prolog = []) {\n        this.instruction = instruction;\n        this.context = context;\n        this.prolog = prolog;\n    }\n    expand() {\n        if (this.#expanded)\n            return;\n        const instructions = unzipContent(this.instruction);\n        let list = [];\n        let id;\n        const maybeList = this.context.at(-1)?.parts?.at(0);\n        const localProlog = this.context.slice(0, -1);\n        const originalListItems = [];\n        if (isListPart(maybeList)) {\n            id = maybeList.id;\n            // console.log(\"LIST PART FOUND\", id);\n            for (const [index, item] of maybeList.list.entries()) {\n                const innerContext = item.content;\n                const innerInstruction = instructions.contents.at(index) ||\n                    instructions.contents.at(0) ||\n                    emptyContent();\n                const innerExpander = new ListExpander(innerInstruction, innerContext, [\n                    ...this.prolog,\n                    ...localProlog,\n                ]);\n                innerExpander.expand();\n                // console.log(\"INDEX\", index);\n                // console.log(\"ITEM\", JSON.stringify(item.content));\n                // console.log(\"INNER EXPANDER\", innerExpander.list());\n                originalListItems.push(innerContext);\n                list.push(...innerExpander.list());\n            }\n            // console.log(\"END LIST PART FOUND\", id);\n        }\n        else {\n            id = instructions.id;\n            list = instructions.contents.map((instruction) => {\n                return {\n                    prolog: [...this.prolog, ...this.context],\n                    instruction,\n                    context: [],\n                };\n            });\n        }\n        this.#prolog = localProlog;\n        this.#list = list;\n        this.#id = id;\n        this.#originalListItems = originalListItems;\n        this.#expanded = true;\n    }\n    list() {\n        return this.#list;\n    }\n    async map(transformer) {\n        this.expand();\n        const isList = this.#list.length > 1;\n        const results = await Promise.all(this.#list.map(async (item, index) => {\n            return transformer(item.instruction, [...item.prolog, ...item.context], isList);\n        }));\n        const errors = [];\n        const successes = [];\n        results.forEach((result) => {\n            if (!ok(result)) {\n                errors.push(result);\n            }\n            else {\n                successes.push(result);\n            }\n        });\n        if (errors.length > 0) {\n            return err(errors.map((error) => error.$error).join(\"\\n\"));\n        }\n        return this.#toContext(successes);\n    }\n    #toContext(results) {\n        if (results.length > 1) {\n            const newListItem = {\n                parts: [\n                    {\n                        id: this.#id,\n                        list: results.map((item, i) => ({\n                            content: [...(this.#originalListItems[i] || []), item],\n                        })),\n                    },\n                ],\n            };\n            return [...this.#prolog, newListItem];\n        }\n        const newContextItem = results.at(-1);\n        return [...this.context, newContextItem];\n    }\n}\nasync function fanOutContext(instruction, context, transformer, path) {\n    context ??= [];\n    const expander = new ListExpander(instruction, context);\n    return expander.map(transformer);\n}\nfunction flattenContext(context, all = false, separator = \"\") {\n    context ??= []; // Look at the first part of the last context and see if it's a list.\n    const last = context.at(-1);\n    if (!last)\n        return context;\n    if (all) {\n        return context\n            .map((content) => flattenContent(content, all, separator))\n            .flat();\n    }\n    const remainder = context.slice(0, -1);\n    return [...remainder, ...flattenContent(last, all, separator)];\n}\nfunction zipContexts(contexts, separator = \"\") {\n    let maxLength = 0;\n    contexts.forEach((context) => {\n        if (maxLength < context.length)\n            maxLength = context.length;\n    });\n    const result = [];\n    for (let i = 0; i < maxLength; i++) {\n        let role;\n        let zippedParts = [];\n        for (const context of contexts) {\n            const item = context.at(i);\n            if (!item)\n                continue;\n            if (!role)\n                role = item.role;\n            zippedParts.push(item.parts);\n            // Add separator if previous element was text.\n            const lastItem = item.parts.slice(-1)[0];\n            if (separator.length > 0 && lastItem && \"text\" in lastItem) {\n                zippedParts.push({ text: separator });\n            }\n        }\n        const parts = mergeTextParts(zippedParts.flat());\n        role ??= \"user\";\n        result.push({\n            parts,\n            role,\n        });\n    }\n    return result;\n}\nfunction flattenContent(content, all = false, separatator = \"\") {\n    let hadList = false;\n    const flattened = content.parts\n        .map((part) => {\n        if (isListPart(part)) {\n            hadList = true;\n            return zipContexts(part.list.map((item) => item.content), separatator);\n        }\n        return {\n            parts: [part],\n            role: content.role,\n        };\n    })\n        .flat();\n    if (!hadList)\n        return [content];\n    return flattened;\n}\nfunction toList(content) {\n    const jsonPart = content.parts.at(0);\n    if (!jsonPart || !(\"json\" in jsonPart)) {\n        // TODO: Error recovery\n        return err(`Gemini generated invalid list`);\n    }\n    const response = jsonPart.json;\n    return {\n        parts: [\n            {\n                id: generateId(),\n                list: response.list.map((item) => {\n                    return { content: [toLLMContent(item, \"model\")] };\n                }),\n            },\n        ],\n    };\n}\nfunction listSchema() {\n    return {\n        type: \"object\",\n        properties: {\n            list: {\n                type: \"array\",\n                description: \"The list of results\",\n                items: {\n                    type: \"string\",\n                    description: \"Result list item as markdown text\",\n                },\n            },\n        },\n        required: [\"list\"],\n    };\n}\nfunction listPrompt(content) {\n    return llm `\n  ${content}\n\n  Output as a list of items, each item must be markdown text.\n`.asContent();\n}\n",
+      "code": "var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n    return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n};\nvar __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {\n    if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\n    return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\n};\nvar _ListExpander_instances, _ListExpander_prolog, _ListExpander_list, _ListExpander_originalListItems, _ListExpander_id, _ListExpander_instructions, _ListExpander_expanded, _ListExpander_toContext;\n/**\n * @fileoverview Handles lists according to https://github.com/breadboard-ai/breadboard/wiki/Step-Listification\n */\nimport { ok, err, generateId, mergeTextParts, toLLMContent, llm, } from \"./utils\";\nexport { fanOutContext, flattenContext, hasLists, addContent, toList, listPrompt, listSchema, ListExpander, };\nfunction isListPart(o) {\n    return !!o && \"list\" in o;\n}\nfunction emptyContent() {\n    return { parts: [{ text: \"\" }] };\n}\nfunction addContent(context, content) {\n    const last = context.at(-1);\n    const maybeList = last?.parts?.at(0);\n    const remainder = context.slice(0, -1);\n    if (isListPart(maybeList)) {\n        const list = maybeList.list.map((item) => ({\n            ...item,\n            content: [...item.content, content],\n        }));\n        return [...remainder, { ...last, parts: [{ ...maybeList, list }] }];\n    }\n    return [...context, content];\n}\nfunction unzipContent(content) {\n    // 1) Scan content for lists.\n    const info = new Set();\n    const ids = new Set();\n    let maxLength = 0;\n    content.parts.forEach((part, index) => {\n        if (!isListPart(part))\n            return;\n        const length = part.list.length;\n        if (length > maxLength)\n            maxLength = length;\n        info.add(index);\n        ids.add(part.id);\n    });\n    if (ids.size > 1) {\n        console.warn(\"Multiple list sources aren't yet supported in instruction, using the first one\");\n    }\n    const id = [...ids].at(0) || \"\";\n    if (info.size === 0) {\n        return { contents: [content], id };\n    }\n    // 2) Create a list and replace lists with list entries.\n    return {\n        id,\n        contents: new Array(maxLength).fill(0).map((_, entryIndex) => {\n            const parts = mergeTextParts(content.parts.flatMap((part, partIndex) => {\n                if (!info.has(partIndex))\n                    return part;\n                // We know this exists, so we're ok with not checking\n                // for existence.\n                const item = part.list.at(entryIndex);\n                if (!item) {\n                    return [];\n                }\n                const last = item.content.at(-1);\n                return last ? last.parts : [];\n            }));\n            return { ...content, parts };\n        }),\n    };\n}\nfunction hasLists(context) {\n    return isListPart(context.at(-1)?.parts?.at(0));\n}\n// TODO: When back to implementing nested lists, this approach\n// is wrong. Instead of expanding items and then converging them,\n// we need to iterate over existing structure. Because then, when\n// it's time to converge, we don't need to try to reconstruct\n// the original structure.\n// Also, it is unclear what we should do with nested lists\n// in instruction. It feels like a similar story, and now\n// we need to support multiple lists and hierarchies in\n// instruction.\nclass ListExpander {\n    constructor(instruction, context, prolog = []) {\n        _ListExpander_instances.add(this);\n        this.instruction = instruction;\n        this.context = context;\n        this.prolog = prolog;\n        // Local prolog -- the parts of context that were preceding the list.\n        _ListExpander_prolog.set(this, []);\n        _ListExpander_list.set(this, []);\n        _ListExpander_originalListItems.set(this, []);\n        _ListExpander_id.set(this, \"\");\n        _ListExpander_instructions.set(this, void 0);\n        _ListExpander_expanded.set(this, false);\n    }\n    expand() {\n        if (__classPrivateFieldGet(this, _ListExpander_expanded, \"f\"))\n            return;\n        const instructions = unzipContent(this.instruction);\n        let list = [];\n        let id;\n        const maybeList = this.context.at(-1)?.parts?.at(0);\n        const localProlog = this.context.slice(0, -1);\n        const originalListItems = [];\n        if (isListPart(maybeList)) {\n            id = maybeList.id;\n            // console.log(\"LIST PART FOUND\", id);\n            for (const [index, item] of maybeList.list.entries()) {\n                const innerContext = item.content;\n                const innerInstruction = instructions.contents.at(index) ||\n                    instructions.contents.at(0) ||\n                    emptyContent();\n                const innerExpander = new ListExpander(innerInstruction, innerContext, [\n                    ...this.prolog,\n                    ...localProlog,\n                ]);\n                innerExpander.expand();\n                // console.log(\"INDEX\", index);\n                // console.log(\"ITEM\", JSON.stringify(item.content));\n                // console.log(\"INNER EXPANDER\", innerExpander.list());\n                originalListItems.push(innerContext);\n                list.push(...innerExpander.list());\n            }\n            // console.log(\"END LIST PART FOUND\", id);\n        }\n        else {\n            id = instructions.id;\n            list = instructions.contents.map((instruction) => {\n                return {\n                    prolog: [...this.prolog, ...this.context],\n                    instruction,\n                    context: [],\n                };\n            });\n        }\n        __classPrivateFieldSet(this, _ListExpander_prolog, localProlog, \"f\");\n        __classPrivateFieldSet(this, _ListExpander_list, list, \"f\");\n        __classPrivateFieldSet(this, _ListExpander_id, id, \"f\");\n        __classPrivateFieldSet(this, _ListExpander_originalListItems, originalListItems, \"f\");\n        __classPrivateFieldSet(this, _ListExpander_expanded, true, \"f\");\n    }\n    list() {\n        return __classPrivateFieldGet(this, _ListExpander_list, \"f\");\n    }\n    async map(transformer) {\n        this.expand();\n        const isList = __classPrivateFieldGet(this, _ListExpander_list, \"f\").length > 1;\n        const results = await Promise.all(__classPrivateFieldGet(this, _ListExpander_list, \"f\").map(async (item, index) => {\n            return transformer(item.instruction, [...item.prolog, ...item.context], isList);\n        }));\n        const errors = [];\n        const successes = [];\n        results.forEach((result) => {\n            if (!ok(result)) {\n                errors.push(result);\n            }\n            else {\n                successes.push(result);\n            }\n        });\n        if (errors.length > 0) {\n            return err(errors.map((error) => error.$error).join(\"\\n\"));\n        }\n        return __classPrivateFieldGet(this, _ListExpander_instances, \"m\", _ListExpander_toContext).call(this, successes);\n    }\n}\n_ListExpander_prolog = new WeakMap(), _ListExpander_list = new WeakMap(), _ListExpander_originalListItems = new WeakMap(), _ListExpander_id = new WeakMap(), _ListExpander_instructions = new WeakMap(), _ListExpander_expanded = new WeakMap(), _ListExpander_instances = new WeakSet(), _ListExpander_toContext = function _ListExpander_toContext(results) {\n    if (results.length > 1) {\n        const newListItem = {\n            parts: [\n                {\n                    id: __classPrivateFieldGet(this, _ListExpander_id, \"f\"),\n                    list: results.map((item, i) => ({\n                        content: [...(__classPrivateFieldGet(this, _ListExpander_originalListItems, \"f\")[i] || []), item],\n                    })),\n                },\n            ],\n        };\n        return [...__classPrivateFieldGet(this, _ListExpander_prolog, \"f\"), newListItem];\n    }\n    const newContextItem = results.at(-1);\n    return [...this.context, newContextItem];\n};\nasync function fanOutContext(instruction, context, transformer, path) {\n    context ?? (context = []);\n    const expander = new ListExpander(instruction, context);\n    return expander.map(transformer);\n}\nfunction flattenContext(context, all = false, separator = \"\") {\n    context ?? (context = []); // Look at the first part of the last context and see if it's a list.\n    const last = context.at(-1);\n    if (!last)\n        return context;\n    if (all) {\n        return context\n            .map((content) => flattenContent(content, all, separator))\n            .flat();\n    }\n    const remainder = context.slice(0, -1);\n    return [...remainder, ...flattenContent(last, all, separator)];\n}\nfunction zipContexts(contexts, separator = \"\") {\n    let maxLength = 0;\n    contexts.forEach((context) => {\n        if (maxLength < context.length)\n            maxLength = context.length;\n    });\n    const result = [];\n    for (let i = 0; i < maxLength; i++) {\n        let role;\n        let zippedParts = [];\n        for (const context of contexts) {\n            const item = context.at(i);\n            if (!item)\n                continue;\n            if (!role)\n                role = item.role;\n            zippedParts.push(item.parts);\n            // Add separator if previous element was text.\n            const lastItem = item.parts.slice(-1)[0];\n            if (separator.length > 0 && lastItem && \"text\" in lastItem) {\n                zippedParts.push({ text: separator });\n            }\n        }\n        const parts = mergeTextParts(zippedParts.flat());\n        role ?? (role = \"user\");\n        result.push({\n            parts,\n            role,\n        });\n    }\n    return result;\n}\nfunction flattenContent(content, all = false, separatator = \"\") {\n    let hadList = false;\n    const flattened = content.parts\n        .map((part) => {\n        if (isListPart(part)) {\n            hadList = true;\n            return zipContexts(part.list.map((item) => item.content), separatator);\n        }\n        return {\n            parts: [part],\n            role: content.role,\n        };\n    })\n        .flat();\n    if (!hadList)\n        return [content];\n    return flattened;\n}\nfunction toList(content) {\n    const jsonPart = content.parts.at(0);\n    if (!jsonPart || !(\"json\" in jsonPart)) {\n        // TODO: Error recovery\n        return err(`Gemini generated invalid list`);\n    }\n    const response = jsonPart.json;\n    return {\n        parts: [\n            {\n                id: generateId(),\n                list: response.list.map((item) => {\n                    return { content: [toLLMContent(item, \"model\")] };\n                }),\n            },\n        ],\n    };\n}\nfunction listSchema() {\n    return {\n        type: \"object\",\n        properties: {\n            list: {\n                type: \"array\",\n                description: \"The list of results\",\n                items: {\n                    type: \"string\",\n                    description: \"Result list item as markdown text\",\n                },\n            },\n        },\n        required: [\"list\"],\n    };\n}\nfunction listPrompt(content) {\n    return llm `\n  ${content}\n\n  Output as a list of items, each item must be markdown text.\n`.asContent();\n}\n",
       "metadata": {
         "title": "lists",
         "source": {
@@ -364,7 +364,7 @@
       }
     },
     "connector-manager": {
-      "code": "/**\n * @fileoverview Manages connectors.\n */\nimport { err, ok, isLLMContentArray } from \"./utils\";\nimport read from \"@read\";\nimport describeConnector, {} from \"@describe\";\nimport invokeConnector from \"@invoke\";\nexport { ConnectorManager, createConfigurator, createTools };\nfunction cx(json) {\n    return { context: [{ parts: [{ json }] }] };\n}\nfunction createTools(handler) {\n    return {\n        invoke: async function (inputs) {\n            const { method, id, info } = inputs;\n            if (method === \"list\") {\n                return handler.list(id, info);\n            }\n            else if (method === \"invoke\") {\n                const { name, args } = inputs;\n                return handler.invoke(id, info, name, args);\n            }\n            return err(`Unknown method: \"${method}\"\"`);\n        },\n        describe: async function () {\n            const { title } = handler;\n            return {\n                title,\n                metadata: {\n                    tags: [\"connector-tools\"],\n                },\n                inputSchema: {\n                    type: \"object\",\n                },\n                outputSchema: {\n                    type: \"object\",\n                },\n            };\n        },\n    };\n}\nfunction createConfigurator(configurator) {\n    return {\n        invoke: createConfiguratorInvoke(configurator),\n        describe: createConfiguratorDescribe(configurator),\n    };\n}\nfunction createConfiguratorDescribe(configurator) {\n    const { title } = configurator;\n    return async function () {\n        return {\n            title: title,\n            description: \"\",\n            metadata: {\n                tags: [\"connector-configure\"],\n            },\n            inputSchema: {\n                type: \"object\",\n            },\n            outputSchema: {\n                type: \"object\",\n                properties: {\n                    context: {\n                        type: \"array\",\n                        items: { type: \"object\", behavior: [\"llm-content\"] },\n                        title: \"Context out\",\n                    },\n                },\n            },\n        };\n    };\n}\nfunction createConfiguratorInvoke(configurator) {\n    return async function ({ context, }) {\n        const inputs = context?.at(-1)?.parts?.at(0)?.json;\n        if (!inputs || !(\"stage\" in inputs)) {\n            return err(`Can't configure ${configurator.title || \"\"} connector: invalid input structure`);\n        }\n        if (inputs.stage === \"initialize\") {\n            const initializing = await configurator.initialize(inputs);\n            if (!ok(initializing))\n                return initializing;\n            return cx(initializing);\n        }\n        else if (inputs.stage === \"read\") {\n            const reading = await configurator.read?.(inputs);\n            if (!reading) {\n                return cx({\n                    schema: {},\n                    values: {},\n                });\n            }\n            if (!ok(reading))\n                return reading;\n            return cx(reading);\n        }\n        else if (inputs.stage === \"preview\") {\n            const previewing = await configurator.preview?.(inputs);\n            if (!previewing || !ok(previewing)) {\n                return { context: [{ parts: [{ text: \"\" }] }] };\n            }\n            return { context: previewing };\n        }\n        else if (inputs.stage === \"write\") {\n            const writing = await configurator.write?.(inputs);\n            if (!writing)\n                return cx({});\n            if (!ok(writing))\n                return writing;\n            return cx(writing);\n        }\n        return err(`Unknown stage: ${inputs[\"stage\"]}`);\n    };\n}\nclass ConnectorManager {\n    part;\n    constructor(part) {\n        this.part = part;\n    }\n    #state = null;\n    async title() {\n        const state = await this.#getState();\n        if (!ok(state))\n            return state;\n        return state.info.url;\n    }\n    async #getState() {\n        const path = `/assets/${this.part.path}`;\n        if (this.#state)\n            return this.#state;\n        const reading = await read({ path });\n        if (!ok(reading))\n            return reading;\n        const info = getConnectorInfo(reading.data);\n        if (!ok(info))\n            return info;\n        const describing = await describeConnector({ url: info.url });\n        if (!ok(describing))\n            return describing;\n        this.#state = { info, describeOutputs: describing };\n        return this.#state;\n    }\n    async #getInvocationArgs(tag) {\n        const state = await this.#getState();\n        if (!ok(state))\n            return state;\n        const url = getExportUrl(tag, state.describeOutputs);\n        if (!ok(url))\n            return url;\n        const id = getConnectorId(this.part);\n        if (!ok(id))\n            return id;\n        return { $board: url, id, info: state.info };\n    }\n    async listTools() {\n        const args = await this.#getInvocationArgs(\"connector-tools\");\n        if (!ok(args))\n            return args;\n        const invoking = await invokeConnector({ method: \"list\", ...args });\n        if (!ok(invoking))\n            return invoking;\n        const output = invoking;\n        console.log(\"LIST TOOLS OUTPUT\", output);\n        return output.list;\n    }\n    async invokeTool(name, args, callTool) {\n        const invocationArgs = await this.#getInvocationArgs(\"connector-tools\");\n        if (!ok(invocationArgs))\n            return invocationArgs;\n        const { id, info } = invocationArgs;\n        await callTool(invocationArgs.$board, {\n            method: \"invoke\",\n            id,\n            info,\n            name,\n            args,\n        }, false, name);\n    }\n    async materialize() {\n        const args = await this.#getInvocationArgs(\"connector-load\");\n        if (!ok(args))\n            return args;\n        const invoking = await invokeConnector(args);\n        if (!ok(invoking))\n            return invoking;\n        const output = invoking;\n        if (output && output.context && isLLMContentArray(output.context)) {\n            return output.context;\n        }\n        return err(`Invalid return value from connector load`);\n    }\n    async schemaProperties() {\n        const args = await this.#getInvocationArgs(\"connector-save\");\n        if (!ok(args))\n            return {};\n        const describing = await describeConnector({ url: args.$board });\n        if (!ok(describing))\n            return {};\n        const props = describing.inputSchema.properties;\n        if (!props || Object.keys(props).length === 0)\n            return {};\n        delete props.context;\n        delete props.id;\n        delete props.info;\n        return props;\n    }\n    async canSave() {\n        const args = await this.#getInvocationArgs(\"connector-save\");\n        if (!ok(args))\n            return false;\n        const invoking = await invokeConnector({\n            ...args,\n            method: \"canSave\",\n        });\n        if (!ok(invoking))\n            return false;\n        return !!invoking.canSave;\n    }\n    async save(context, options) {\n        const args = await this.#getInvocationArgs(\"connector-save\");\n        if (!ok(args))\n            return args;\n        const invoking = await invokeConnector({\n            ...args,\n            context,\n            ...options,\n            method: \"save\",\n        });\n        if (!ok(invoking))\n            return invoking;\n    }\n    static isConnector(part) {\n        return part.path.startsWith(\"connectors/\");\n    }\n}\nfunction getConnectorId(part) {\n    const id = part.path.split(\"/\").at(-1);\n    if (!id)\n        return err(`Invalid connector path: ${part.path}`);\n    return id;\n}\nfunction getExportUrl(tag, result) {\n    const exports = result.exports;\n    if (!exports)\n        return err(`Invalid connector structure: must have exports`);\n    const assetExport = Object.entries(exports).find(([url, e]) => e.metadata?.tags?.includes(tag));\n    if (!assetExport)\n        return err(`Invalid connector structure: must have export tagged as \"${tag}\"`);\n    return assetExport[0];\n}\nfunction getConnectorInfo(data) {\n    const part = data?.at(-1)?.parts.at(0);\n    if (!part)\n        return err(`Invalid asset structure`);\n    if (!(\"json\" in part))\n        return err(`Invalid connector info structure`);\n    return part.json;\n}\n",
+      "code": "/**\n * @fileoverview Manages connectors.\n */\nvar __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n    return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n};\nvar __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {\n    if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\n    return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\n};\nvar _ConnectorManager_instances, _ConnectorManager_state, _ConnectorManager_getState, _ConnectorManager_getInvocationArgs;\nimport { err, ok, isLLMContentArray } from \"./utils\";\nimport read from \"@read\";\nimport describeConnector from \"@describe\";\nimport invokeConnector from \"@invoke\";\nexport { ConnectorManager, createConfigurator, createTools };\nfunction cx(json) {\n    return { context: [{ parts: [{ json }] }] };\n}\nfunction createTools(handler) {\n    return {\n        invoke: async function (inputs) {\n            const { method, id, info } = inputs;\n            if (method === \"list\") {\n                return handler.list(id, info);\n            }\n            else if (method === \"invoke\") {\n                const { name, args } = inputs;\n                return handler.invoke(id, info, name, args);\n            }\n            return err(`Unknown method: \"${method}\"\"`);\n        },\n        describe: async function () {\n            const { title } = handler;\n            return {\n                title,\n                metadata: {\n                    tags: [\"connector-tools\"],\n                },\n                inputSchema: {\n                    type: \"object\",\n                },\n                outputSchema: {\n                    type: \"object\",\n                },\n            };\n        },\n    };\n}\nfunction createConfigurator(configurator) {\n    return {\n        invoke: createConfiguratorInvoke(configurator),\n        describe: createConfiguratorDescribe(configurator),\n    };\n}\nfunction createConfiguratorDescribe(configurator) {\n    const { title } = configurator;\n    return async function () {\n        return {\n            title: title,\n            description: \"\",\n            metadata: {\n                tags: [\"connector-configure\"],\n            },\n            inputSchema: {\n                type: \"object\",\n            },\n            outputSchema: {\n                type: \"object\",\n                properties: {\n                    context: {\n                        type: \"array\",\n                        items: { type: \"object\", behavior: [\"llm-content\"] },\n                        title: \"Context out\",\n                    },\n                },\n            },\n        };\n    };\n}\nfunction createConfiguratorInvoke(configurator) {\n    return async function ({ context, }) {\n        const inputs = context?.at(-1)?.parts?.at(0)?.json;\n        if (!inputs || !(\"stage\" in inputs)) {\n            return err(`Can't configure ${configurator.title || \"\"} connector: invalid input structure`);\n        }\n        if (inputs.stage === \"initialize\") {\n            const initializing = await configurator.initialize(inputs);\n            if (!ok(initializing))\n                return initializing;\n            return cx(initializing);\n        }\n        else if (inputs.stage === \"read\") {\n            const reading = await configurator.read?.(inputs);\n            if (!reading) {\n                return cx({\n                    schema: {},\n                    values: {},\n                });\n            }\n            if (!ok(reading))\n                return reading;\n            return cx(reading);\n        }\n        else if (inputs.stage === \"preview\") {\n            const previewing = await configurator.preview?.(inputs);\n            if (!previewing || !ok(previewing)) {\n                return { context: [{ parts: [{ text: \"\" }] }] };\n            }\n            return { context: previewing };\n        }\n        else if (inputs.stage === \"write\") {\n            const writing = await configurator.write?.(inputs);\n            if (!writing)\n                return cx({});\n            if (!ok(writing))\n                return writing;\n            return cx(writing);\n        }\n        return err(`Unknown stage: ${inputs[\"stage\"]}`);\n    };\n}\nclass ConnectorManager {\n    constructor(part) {\n        _ConnectorManager_instances.add(this);\n        this.part = part;\n        _ConnectorManager_state.set(this, null);\n    }\n    async title() {\n        const state = await __classPrivateFieldGet(this, _ConnectorManager_instances, \"m\", _ConnectorManager_getState).call(this);\n        if (!ok(state))\n            return state;\n        return state.info.url;\n    }\n    async listTools() {\n        const args = await __classPrivateFieldGet(this, _ConnectorManager_instances, \"m\", _ConnectorManager_getInvocationArgs).call(this, \"connector-tools\");\n        if (!ok(args))\n            return args;\n        const invoking = await invokeConnector({ method: \"list\", ...args });\n        if (!ok(invoking))\n            return invoking;\n        const output = invoking;\n        console.log(\"LIST TOOLS OUTPUT\", output);\n        return output.list;\n    }\n    async invokeTool(name, args, callTool) {\n        const invocationArgs = await __classPrivateFieldGet(this, _ConnectorManager_instances, \"m\", _ConnectorManager_getInvocationArgs).call(this, \"connector-tools\");\n        if (!ok(invocationArgs))\n            return invocationArgs;\n        const { id, info } = invocationArgs;\n        await callTool(invocationArgs.$board, {\n            method: \"invoke\",\n            id,\n            info,\n            name,\n            args,\n        }, false, name);\n    }\n    async materialize() {\n        const args = await __classPrivateFieldGet(this, _ConnectorManager_instances, \"m\", _ConnectorManager_getInvocationArgs).call(this, \"connector-load\");\n        if (!ok(args))\n            return args;\n        const invoking = await invokeConnector(args);\n        if (!ok(invoking))\n            return invoking;\n        const output = invoking;\n        if (output && output.context && isLLMContentArray(output.context)) {\n            return output.context;\n        }\n        return err(`Invalid return value from connector load`);\n    }\n    async schemaProperties() {\n        const args = await __classPrivateFieldGet(this, _ConnectorManager_instances, \"m\", _ConnectorManager_getInvocationArgs).call(this, \"connector-save\");\n        if (!ok(args))\n            return {};\n        const describing = await describeConnector({ url: args.$board });\n        if (!ok(describing))\n            return {};\n        const props = describing.inputSchema.properties;\n        if (!props || Object.keys(props).length === 0)\n            return {};\n        delete props.context;\n        delete props.id;\n        delete props.info;\n        return props;\n    }\n    async canSave() {\n        const args = await __classPrivateFieldGet(this, _ConnectorManager_instances, \"m\", _ConnectorManager_getInvocationArgs).call(this, \"connector-save\");\n        if (!ok(args))\n            return false;\n        const invoking = await invokeConnector({\n            ...args,\n            method: \"canSave\",\n        });\n        if (!ok(invoking))\n            return false;\n        return !!invoking.canSave;\n    }\n    async save(context, options) {\n        const args = await __classPrivateFieldGet(this, _ConnectorManager_instances, \"m\", _ConnectorManager_getInvocationArgs).call(this, \"connector-save\");\n        if (!ok(args))\n            return args;\n        const invoking = await invokeConnector({\n            ...args,\n            context,\n            ...options,\n            method: \"save\",\n        });\n        if (!ok(invoking))\n            return invoking;\n    }\n    static isConnector(part) {\n        return part.path.startsWith(\"connectors/\");\n    }\n}\n_ConnectorManager_state = new WeakMap(), _ConnectorManager_instances = new WeakSet(), _ConnectorManager_getState = async function _ConnectorManager_getState() {\n    const path = `/assets/${this.part.path}`;\n    if (__classPrivateFieldGet(this, _ConnectorManager_state, \"f\"))\n        return __classPrivateFieldGet(this, _ConnectorManager_state, \"f\");\n    const reading = await read({ path });\n    if (!ok(reading))\n        return reading;\n    const info = getConnectorInfo(reading.data);\n    if (!ok(info))\n        return info;\n    const describing = await describeConnector({ url: info.url });\n    if (!ok(describing))\n        return describing;\n    __classPrivateFieldSet(this, _ConnectorManager_state, { info, describeOutputs: describing }, \"f\");\n    return __classPrivateFieldGet(this, _ConnectorManager_state, \"f\");\n}, _ConnectorManager_getInvocationArgs = async function _ConnectorManager_getInvocationArgs(tag) {\n    const state = await __classPrivateFieldGet(this, _ConnectorManager_instances, \"m\", _ConnectorManager_getState).call(this);\n    if (!ok(state))\n        return state;\n    const url = getExportUrl(tag, state.describeOutputs);\n    if (!ok(url))\n        return url;\n    const id = getConnectorId(this.part);\n    if (!ok(id))\n        return id;\n    return { $board: url, id, info: state.info };\n};\nfunction getConnectorId(part) {\n    const id = part.path.split(\"/\").at(-1);\n    if (!id)\n        return err(`Invalid connector path: ${part.path}`);\n    return id;\n}\nfunction getExportUrl(tag, result) {\n    const exports = result.exports;\n    if (!exports)\n        return err(`Invalid connector structure: must have exports`);\n    const assetExport = Object.entries(exports).find(([url, e]) => e.metadata?.tags?.includes(tag));\n    if (!assetExport)\n        return err(`Invalid connector structure: must have export tagged as \"${tag}\"`);\n    return assetExport[0];\n}\nfunction getConnectorInfo(data) {\n    const part = data?.at(-1)?.parts.at(0);\n    if (!part)\n        return err(`Invalid asset structure`);\n    if (!(\"json\" in part))\n        return err(`Invalid connector info structure`);\n    return part.json;\n}\n",
       "metadata": {
         "title": "connector-manager",
         "source": {
@@ -376,7 +376,7 @@
       }
     },
     "image-utils": {
-      "code": "/**\n * @fileoverview Utilities for generating images.\n */\nimport { err, ok, isStoredData, toLLMContent, toLLMContentInline, toLLMContentStored, toInlineReference, toInlineData, toText, addUserTurn, llm, } from \"./utils\";\nimport { executeStep, } from \"./step-executor\";\nimport { GeminiPrompt } from \"./gemini-prompt\";\nexport { callImageGen, callGeminiImage, promptExpander };\nconst STEP_NAME = \"AI Image Tool\";\nconst OUTPUT_NAME = \"generated_image\";\nconst API_NAME = \"ai_image_tool\";\nasync function callGeminiImage(instruction, imageContent, disablePromptRewrite, aspectRatio = \"1:1\") {\n    const imageChunks = [];\n    for (const element of imageContent) {\n        let inlineChunk;\n        if (isStoredData(element)) {\n            inlineChunk = toInlineReference(element);\n        }\n        else {\n            inlineChunk = toInlineData(element);\n        }\n        if (inlineChunk && inlineChunk != null && typeof inlineChunk != \"string\") {\n            imageChunks.push({\n                mimetype: inlineChunk.mimeType,\n                data: inlineChunk.data,\n            });\n        }\n    }\n    const input_parameters = [\"input_instruction\"];\n    if (imageChunks.length > 0) {\n        input_parameters.push(\"input_image\");\n    }\n    console.log(\"Number of input images: \" + String(imageChunks.length));\n    const encodedInstruction = btoa(unescape(encodeURIComponent(instruction)));\n    const executionInputs = {\n        input_instruction: {\n            chunks: [\n                {\n                    mimetype: \"text/plain\",\n                    data: encodedInstruction,\n                },\n            ],\n        },\n        aspect_ratio_key: {\n            chunks: [\n                {\n                    mimetype: \"text/plain\",\n                    data: btoa(aspectRatio),\n                },\n            ],\n        },\n    };\n    if (imageChunks.length > 0) {\n        executionInputs[\"input_image\"] = {\n            chunks: imageChunks,\n        };\n    }\n    const body = {\n        planStep: {\n            stepName: STEP_NAME,\n            modelApi: API_NAME,\n            inputParameters: input_parameters,\n            systemPrompt: \"\",\n            options: {\n                disablePromptRewrite: disablePromptRewrite,\n            },\n            output: OUTPUT_NAME,\n        },\n        execution_inputs: executionInputs,\n    };\n    // TODO(askerryryan): Remove once functional.\n    console.log(\"request body\");\n    console.log(body);\n    const response = await executeStep(body);\n    // TODO(askerryryan): Remove once functional.\n    console.log(\"response\");\n    console.log(response);\n    if (!ok(response)) {\n        return err(\"Image generation failed. \" +\n            response.$error +\n            \" Check your prompt to ensure it is a valid and compliant image prompt.\");\n    }\n    const outContent = response.executionOutputs && response.executionOutputs[OUTPUT_NAME];\n    if (!outContent) {\n        return err(\"Error: No image returned from backend\");\n    }\n    return outContent.chunks.map((c) => {\n        if (c.mimetype.endsWith(\"/storedData\")) {\n            return toLLMContentStored(c.mimetype.replace(\"/storedData\", \"\"), c.data);\n        }\n        return toLLMContentInline(c.mimetype, c.data);\n    });\n}\nasync function callImageGen(imageInstruction, aspectRatio = \"1:1\") {\n    const executionInputs = {};\n    const encodedInstruction = btoa(unescape(encodeURIComponent(imageInstruction)));\n    executionInputs[\"image_prompt\"] = {\n        chunks: [\n            {\n                mimetype: \"text/plain\",\n                data: encodedInstruction,\n            },\n        ],\n    };\n    executionInputs[\"aspect_ratio_key\"] = {\n        chunks: [\n            {\n                mimetype: \"text/plain\",\n                data: btoa(aspectRatio),\n            },\n        ],\n    };\n    const inputParameters = [\"image_prompt\"];\n    const body = {\n        planStep: {\n            stepName: \"GenerateImage\",\n            modelApi: \"image_generation\",\n            inputParameters: inputParameters,\n            systemPrompt: \"\",\n            output: OUTPUT_NAME,\n        },\n        execution_inputs: executionInputs,\n    };\n    const response = await executeStep(body);\n    console.log(response);\n    if (!ok(response)) {\n        return err(\"Image generation failed. \" + response.$error);\n    }\n    const outContent = response.executionOutputs && response.executionOutputs[OUTPUT_NAME];\n    if (!outContent) {\n        return err(\"Error: No image returned from backend\");\n    }\n    return outContent.chunks.map((c) => {\n        if (c.mimetype.endsWith(\"/storedData\")) {\n            return toLLMContentStored(c.mimetype.replace(\"/storedData\", \"\"), c.data);\n        }\n        return toLLMContentInline(c.mimetype, c.data);\n    });\n}\nfunction promptExpander(contents, instruction) {\n    const context = contents?.length\n        ? \"using conversation context and these additional\"\n        : \"with these\";\n    const promptText = llm `Generate a single text-to-image prompt ${context} instructions:\n${instruction}\n\nTypical output format:\n\"\"\"\nCreate the following image:\n\n## Setting/background\n\n<Detailed description of everything that is in the background of the image.>\n\n## Foreground/focus\n\n<Detailed description of object and/or shapes that are in the foreground and are the main focal point of the image>\n\n## Style\n\n<Detailed description of the style, color scheme, vibe, kind of drawing (illustration, photorealistic, etc.)>\n\nYou output will be fed directly into the text-to-image model, so it must be prompt only, no additional chit-chat\n\"\"\"\n`;\n    return new GeminiPrompt({\n        body: {\n            contents: addUserTurn(promptText.asContent(), contents),\n            systemInstruction: toLLMContent(`\nYou are a creative writer whose specialty is to write prompts for text-to-image models.\n\nThe prompt must describe every object in the image in great detail and describe the style\nin terms of color scheme and vibe.\n`),\n        },\n    });\n}\n",
+      "code": "/**\n * @fileoverview Utilities for generating images.\n */\nimport { err, ok, isStoredData, toLLMContent, toLLMContentInline, toLLMContentStored, toInlineReference, toInlineData, addUserTurn, llm, } from \"./utils\";\nimport { executeStep, } from \"./step-executor\";\nimport { GeminiPrompt } from \"./gemini-prompt\";\nexport { callImageGen, callGeminiImage, promptExpander };\nconst STEP_NAME = \"AI Image Tool\";\nconst OUTPUT_NAME = \"generated_image\";\nconst API_NAME = \"ai_image_tool\";\nasync function callGeminiImage(instruction, imageContent, disablePromptRewrite, aspectRatio = \"1:1\") {\n    const imageChunks = [];\n    for (const element of imageContent) {\n        let inlineChunk;\n        if (isStoredData(element)) {\n            inlineChunk = toInlineReference(element);\n        }\n        else {\n            inlineChunk = toInlineData(element);\n        }\n        if (inlineChunk && inlineChunk != null && typeof inlineChunk != \"string\") {\n            imageChunks.push({\n                mimetype: inlineChunk.mimeType,\n                data: inlineChunk.data,\n            });\n        }\n    }\n    const input_parameters = [\"input_instruction\"];\n    if (imageChunks.length > 0) {\n        input_parameters.push(\"input_image\");\n    }\n    console.log(\"Number of input images: \" + String(imageChunks.length));\n    const encodedInstruction = btoa(unescape(encodeURIComponent(instruction)));\n    const executionInputs = {\n        input_instruction: {\n            chunks: [\n                {\n                    mimetype: \"text/plain\",\n                    data: encodedInstruction,\n                },\n            ],\n        },\n        aspect_ratio_key: {\n            chunks: [\n                {\n                    mimetype: \"text/plain\",\n                    data: btoa(aspectRatio),\n                },\n            ],\n        },\n    };\n    if (imageChunks.length > 0) {\n        executionInputs[\"input_image\"] = {\n            chunks: imageChunks,\n        };\n    }\n    const body = {\n        planStep: {\n            stepName: STEP_NAME,\n            modelApi: API_NAME,\n            inputParameters: input_parameters,\n            systemPrompt: \"\",\n            options: {\n                disablePromptRewrite: disablePromptRewrite,\n            },\n            output: OUTPUT_NAME,\n        },\n        execution_inputs: executionInputs,\n    };\n    // TODO(askerryryan): Remove once functional.\n    console.log(\"request body\");\n    console.log(body);\n    const response = await executeStep(body);\n    // TODO(askerryryan): Remove once functional.\n    console.log(\"response\");\n    console.log(response);\n    if (!ok(response)) {\n        return err(\"Image generation failed. \" +\n            response.$error +\n            \" Check your prompt to ensure it is a valid and compliant image prompt.\");\n    }\n    const outContent = response.executionOutputs && response.executionOutputs[OUTPUT_NAME];\n    if (!outContent) {\n        return err(\"Error: No image returned from backend\");\n    }\n    return outContent.chunks.map((c) => {\n        if (c.mimetype.endsWith(\"/storedData\")) {\n            return toLLMContentStored(c.mimetype.replace(\"/storedData\", \"\"), c.data);\n        }\n        return toLLMContentInline(c.mimetype, c.data);\n    });\n}\nasync function callImageGen(imageInstruction, aspectRatio = \"1:1\") {\n    const executionInputs = {};\n    const encodedInstruction = btoa(unescape(encodeURIComponent(imageInstruction)));\n    executionInputs[\"image_prompt\"] = {\n        chunks: [\n            {\n                mimetype: \"text/plain\",\n                data: encodedInstruction,\n            },\n        ],\n    };\n    executionInputs[\"aspect_ratio_key\"] = {\n        chunks: [\n            {\n                mimetype: \"text/plain\",\n                data: btoa(aspectRatio),\n            },\n        ],\n    };\n    const inputParameters = [\"image_prompt\"];\n    const body = {\n        planStep: {\n            stepName: \"GenerateImage\",\n            modelApi: \"image_generation\",\n            inputParameters: inputParameters,\n            systemPrompt: \"\",\n            output: OUTPUT_NAME,\n        },\n        execution_inputs: executionInputs,\n    };\n    const response = await executeStep(body);\n    console.log(response);\n    if (!ok(response)) {\n        return err(\"Image generation failed. \" + response.$error);\n    }\n    const outContent = response.executionOutputs && response.executionOutputs[OUTPUT_NAME];\n    if (!outContent) {\n        return err(\"Error: No image returned from backend\");\n    }\n    return outContent.chunks.map((c) => {\n        if (c.mimetype.endsWith(\"/storedData\")) {\n            return toLLMContentStored(c.mimetype.replace(\"/storedData\", \"\"), c.data);\n        }\n        return toLLMContentInline(c.mimetype, c.data);\n    });\n}\nfunction promptExpander(contents, instruction) {\n    const context = contents?.length\n        ? \"using conversation context and these additional\"\n        : \"with these\";\n    const promptText = llm `Generate a single text-to-image prompt ${context} instructions:\n${instruction}\n\nTypical output format:\n\"\"\"\nCreate the following image:\n\n## Setting/background\n\n<Detailed description of everything that is in the background of the image.>\n\n## Foreground/focus\n\n<Detailed description of object and/or shapes that are in the foreground and are the main focal point of the image>\n\n## Style\n\n<Detailed description of the style, color scheme, vibe, kind of drawing (illustration, photorealistic, etc.)>\n\nYou output will be fed directly into the text-to-image model, so it must be prompt only, no additional chit-chat\n\"\"\"\n`;\n    return new GeminiPrompt({\n        body: {\n            contents: addUserTurn(promptText.asContent(), contents),\n            systemInstruction: toLLMContent(`\nYou are a creative writer whose specialty is to write prompts for text-to-image models.\n\nThe prompt must describe every object in the image in great detail and describe the style\nin terms of color scheme and vibe.\n`),\n        },\n    });\n}\n",
       "metadata": {
         "title": "image-utils",
         "source": {
diff --git a/packages/a2/bgl/audio-generator.bgl.json b/packages/a2/bgl/audio-generator.bgl.json
index cb7ef048c3b..62b3bf0ce1a 100644
--- a/packages/a2/bgl/audio-generator.bgl.json
+++ b/packages/a2/bgl/audio-generator.bgl.json
@@ -51,7 +51,7 @@
   },
   "modules": {
     "main": {
-      "code": "/**\n * @fileoverview Generates audio (tts) output using supplied context.\n */\nimport gemini, { defaultSafetySettings, } from \"./a2/gemini\";\nimport { err, ok, llm, toLLMContent, toLLMContentInline, toLLMContentStored, toTextConcat, joinContent, toText, defaultLLMContent, } from \"./a2/utils\";\nimport { Template } from \"./a2/template\";\nimport { ToolManager } from \"./a2/tool-manager\";\nimport {} from \"./a2/common\";\nimport { ArgumentNameGenerator } from \"./a2/introducer\";\nimport { executeStep, } from \"./a2/step-executor\";\nimport { ListExpander } from \"./a2/lists\";\nconst VoiceMap = {\n    \"Male (English)\": \"en-US-male\",\n    \"Female (English)\": \"en-US-female\",\n}; // Use 'as const' for stricter type inference\nconst VOICES = Object.keys(VoiceMap);\nexport { invoke as default, describe };\nasync function callAudioGen(prompt, voice) {\n    let voiceParam = \"en-US-female\";\n    if (voice in VoiceMap) {\n        voiceParam = VoiceMap[voice];\n    }\n    const executionInputs = {};\n    const encodedPrompt = btoa(unescape(encodeURIComponent(prompt)));\n    executionInputs[\"text_to_speak\"] = {\n        chunks: [\n            {\n                mimetype: \"text/plain\",\n                data: encodedPrompt,\n            },\n        ],\n    };\n    executionInputs[\"voice_key\"] = {\n        chunks: [\n            {\n                mimetype: \"text/plain\",\n                data: btoa(voiceParam),\n            },\n        ],\n    };\n    const inputParameters = [\"text_to_speak\"];\n    const body = {\n        planStep: {\n            stepName: \"GenerateAudio\",\n            modelApi: \"tts\",\n            inputParameters: inputParameters,\n            systemPrompt: \"\",\n            output: \"generated_speech\",\n        },\n        execution_inputs: executionInputs,\n    };\n    const response = await executeStep(body);\n    if (!ok(response)) {\n        return toLLMContent(\"TTS generation failed: \" + response.$error);\n    }\n    if (!response.executionOutputs) {\n        return toLLMContent(\"TTS returned no audio\");\n    }\n    let returnVal;\n    for (let value of Object.values(response.executionOutputs)) {\n        const mimetype = value.chunks[0].mimetype;\n        if (mimetype.startsWith(\"audio\")) {\n            if (mimetype.endsWith(\"/storedData\")) {\n                returnVal = toLLMContentStored(mimetype.replace(\"/storedData\", \"\"), value.chunks[0].data);\n            }\n            else {\n                returnVal = toLLMContentInline(mimetype, value.chunks[0].data);\n            }\n        }\n    }\n    if (!returnVal) {\n        return toLLMContent(\"Error: No audio returned from backend\");\n    }\n    console.log(returnVal);\n    return returnVal;\n}\nasync function invoke({ context, text, voice, ...params }) {\n    context ??= [];\n    let instructionText = \"\";\n    if (text) {\n        instructionText = toText(text).trim();\n    }\n    const template = new Template(toLLMContent(instructionText));\n    const toolManager = new ToolManager(new ArgumentNameGenerator());\n    const substituting = await template.substitute(params, async ({ path: url, instance }) => toolManager.addTool(url, instance));\n    if (!ok(substituting)) {\n        return substituting;\n    }\n    console.log(\"context\");\n    console.log(context);\n    console.log(\"instruction\");\n    console.log(text);\n    console.log(\"substituting\");\n    console.log(substituting);\n    const results = await new ListExpander(substituting, context).map(async (itemInstruction, itemContext) => {\n        const combinedInstruction = toTextConcat(joinContent(toText(itemInstruction), itemContext, false));\n        if (!combinedInstruction) {\n            return toLLMContent(\"Please provide the text to be converted to speech.\");\n        }\n        console.log(\"PROMPT: \", combinedInstruction);\n        return callAudioGen(combinedInstruction, voice);\n    });\n    if (!ok(results))\n        return results;\n    return { context: results };\n}\nasync function describe({ inputs: { text } }) {\n    const template = new Template(text);\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                text: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-preview\"],\n                    title: \"Text\",\n                    description: \"Construct the inputs to be spoken with text-to-speech. Use @ to reference previous step outputs.\",\n                    default: defaultLLMContent(),\n                },\n                voice: {\n                    type: \"string\",\n                    behavior: [\"hint-text\", \"config\"],\n                    title: \"Voice\",\n                    icon: \"voice-selection\",\n                    enum: VOICES,\n                    description: \"The voice you'd like to generate with\",\n                    default: \"Female (English)\",\n                },\n                ...template.schemas(),\n            },\n            behavior: [\"at-wireable\"],\n            ...template.requireds(),\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"hint-audio\", \"main-port\"],\n                },\n            },\n            additionalProperties: false,\n        },\n        title: \"Make Speech\",\n        metadata: {\n            icon: \"generative-audio\",\n            tags: [\"quick-access\", \"generative\"],\n            order: 3,\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Generates audio (tts) output using supplied context.\n */\nimport { ok, toLLMContent, toLLMContentInline, toLLMContentStored, toTextConcat, joinContent, toText, defaultLLMContent, } from \"./a2/utils\";\nimport { Template } from \"./a2/template\";\nimport { ToolManager } from \"./a2/tool-manager\";\nimport { ArgumentNameGenerator } from \"./a2/introducer\";\nimport { executeStep, } from \"./a2/step-executor\";\nimport { ListExpander } from \"./a2/lists\";\nconst VoiceMap = {\n    \"Male (English)\": \"en-US-male\",\n    \"Female (English)\": \"en-US-female\",\n}; // Use 'as const' for stricter type inference\nconst VOICES = Object.keys(VoiceMap);\nexport { invoke as default, describe };\nasync function callAudioGen(prompt, voice) {\n    let voiceParam = \"en-US-female\";\n    if (voice in VoiceMap) {\n        voiceParam = VoiceMap[voice];\n    }\n    const executionInputs = {};\n    const encodedPrompt = btoa(unescape(encodeURIComponent(prompt)));\n    executionInputs[\"text_to_speak\"] = {\n        chunks: [\n            {\n                mimetype: \"text/plain\",\n                data: encodedPrompt,\n            },\n        ],\n    };\n    executionInputs[\"voice_key\"] = {\n        chunks: [\n            {\n                mimetype: \"text/plain\",\n                data: btoa(voiceParam),\n            },\n        ],\n    };\n    const inputParameters = [\"text_to_speak\"];\n    const body = {\n        planStep: {\n            stepName: \"GenerateAudio\",\n            modelApi: \"tts\",\n            inputParameters: inputParameters,\n            systemPrompt: \"\",\n            output: \"generated_speech\",\n        },\n        execution_inputs: executionInputs,\n    };\n    const response = await executeStep(body);\n    if (!ok(response)) {\n        return toLLMContent(\"TTS generation failed: \" + response.$error);\n    }\n    if (!response.executionOutputs) {\n        return toLLMContent(\"TTS returned no audio\");\n    }\n    let returnVal;\n    for (let value of Object.values(response.executionOutputs)) {\n        const mimetype = value.chunks[0].mimetype;\n        if (mimetype.startsWith(\"audio\")) {\n            if (mimetype.endsWith(\"/storedData\")) {\n                returnVal = toLLMContentStored(mimetype.replace(\"/storedData\", \"\"), value.chunks[0].data);\n            }\n            else {\n                returnVal = toLLMContentInline(mimetype, value.chunks[0].data);\n            }\n        }\n    }\n    if (!returnVal) {\n        return toLLMContent(\"Error: No audio returned from backend\");\n    }\n    console.log(returnVal);\n    return returnVal;\n}\nasync function invoke({ context, text, voice, ...params }) {\n    context ?? (context = []);\n    let instructionText = \"\";\n    if (text) {\n        instructionText = toText(text).trim();\n    }\n    const template = new Template(toLLMContent(instructionText));\n    const toolManager = new ToolManager(new ArgumentNameGenerator());\n    const substituting = await template.substitute(params, async ({ path: url, instance }) => toolManager.addTool(url, instance));\n    if (!ok(substituting)) {\n        return substituting;\n    }\n    console.log(\"context\");\n    console.log(context);\n    console.log(\"instruction\");\n    console.log(text);\n    console.log(\"substituting\");\n    console.log(substituting);\n    const results = await new ListExpander(substituting, context).map(async (itemInstruction, itemContext) => {\n        const combinedInstruction = toTextConcat(joinContent(toText(itemInstruction), itemContext, false));\n        if (!combinedInstruction) {\n            return toLLMContent(\"Please provide the text to be converted to speech.\");\n        }\n        console.log(\"PROMPT: \", combinedInstruction);\n        return callAudioGen(combinedInstruction, voice);\n    });\n    if (!ok(results))\n        return results;\n    return { context: results };\n}\nasync function describe({ inputs: { text } }) {\n    const template = new Template(text);\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                text: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-preview\"],\n                    title: \"Text\",\n                    description: \"Construct the inputs to be spoken with text-to-speech. Use @ to reference previous step outputs.\",\n                    default: defaultLLMContent(),\n                },\n                voice: {\n                    type: \"string\",\n                    behavior: [\"hint-text\", \"config\"],\n                    title: \"Voice\",\n                    icon: \"voice-selection\",\n                    enum: VOICES,\n                    description: \"The voice you'd like to generate with\",\n                    default: \"Female (English)\",\n                },\n                ...template.schemas(),\n            },\n            behavior: [\"at-wireable\"],\n            ...template.requireds(),\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"hint-audio\", \"main-port\"],\n                },\n            },\n            additionalProperties: false,\n        },\n        title: \"Make Speech\",\n        metadata: {\n            icon: \"generative-audio\",\n            tags: [\"quick-access\", \"generative\"],\n            order: 3,\n        },\n    };\n}\n",
       "metadata": {
         "title": "main",
         "source": {
diff --git a/packages/a2/bgl/fetch-workbench.bgl.json b/packages/a2/bgl/fetch-workbench.bgl.json
index 00d81d68a00..77e37f55b5f 100644
--- a/packages/a2/bgl/fetch-workbench.bgl.json
+++ b/packages/a2/bgl/fetch-workbench.bgl.json
@@ -73,7 +73,7 @@
   },
   "modules": {
     "main": {
-      "code": "/**\n * @fileoverview Add a description for your module here.\n */\nimport fetch from \"@fetch\";\nimport read from \"@read\";\nimport write from \"@write\";\nimport query from \"@query\";\nimport output from \"@output\";\nimport { ok, err } from \"./a2/utils\";\nimport { report } from \"./a2/output\";\nexport { invoke as default, describe };\nfunction json(result) {\n    if (!ok(result))\n        return result;\n    const j = result.data?.at(-1)?.parts.at(0)?.json;\n    if (!j) {\n        return err(`Invalid result structure`);\n    }\n    return j;\n}\nfunction sse(result) {\n    const e = json(result);\n    if (!ok(e))\n        return e;\n    return e.data;\n}\nfunction rpc(result) {\n    const e = sse(result);\n    if (!ok(e))\n        return e;\n    return e.result;\n}\nclass McpClient {\n    connectorId;\n    url;\n    #id = 0;\n    #messageEndpoint = null;\n    constructor(connectorId, url) {\n        this.connectorId = connectorId;\n        this.url = url;\n    }\n    #path() {\n        return `/session/mcp/${this.connectorId}/stream`;\n    }\n    #sessionIdPath() {\n        return `/session/mcp/${this.connectorId}/id`;\n    }\n    #newId() {\n        return ++this.#id;\n    }\n    async connect() {\n        if (this.#messageEndpoint) {\n            return err(`Already connected to \"${this.#messageEndpoint}\"`);\n        }\n        const file = this.#path();\n        const url = this.url;\n        const savedSession = json(await read({ path: this.#sessionIdPath() }));\n        if (ok(savedSession)) {\n            console.log(\"EXISTING SESSION\", savedSession);\n            this.#messageEndpoint = savedSession.endpoint;\n            return;\n        }\n        // Establish connection.\n        const connecting = await fetch({ url, file, stream: \"sse\" });\n        if (!ok(connecting))\n            return connecting;\n        // This is the connection path.\n        const path = connecting.response;\n        // Read the endpoint event\n        const connection = sse(await read({ path }));\n        if (!ok(connection))\n            return connection;\n        // do some crude URL munging.\n        const hostname = url.split(\"/\").slice(0, -1).join(\"/\");\n        const messageEndpoint = `${hostname}${connection}`;\n        const id = this.#newId();\n        // send initialize request\n        const initializing = await fetch({\n            url: messageEndpoint,\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({\n                jsonrpc: \"2.0\",\n                id,\n                method: \"initialize\",\n                params: {\n                    protocolVersion: \"2024-11-05\",\n                    clientInfo: {\n                        name: \"Breadboard\",\n                        version: \"1.0.0\",\n                    },\n                    capabilities: {\n                        tools: {},\n                    },\n                },\n            }),\n        });\n        if (!ok(initializing))\n            return initializing;\n        const initializeResponse = rpc(await read({ path }));\n        if (!ok(initializeResponse))\n            return initializeResponse;\n        const confirmInitialization = await fetch({\n            url: messageEndpoint,\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({\n                jsonrpc: \"2.0\",\n                method: \"notifications/initialized\",\n            }),\n        });\n        if (!ok(confirmInitialization))\n            return confirmInitialization;\n        this.#messageEndpoint = messageEndpoint;\n        // Save message endpoint\n        const savingMessageEndpoint = await write({\n            path: this.#sessionIdPath(),\n            data: [\n                {\n                    parts: [{ json: { endpoint: messageEndpoint } }],\n                },\n            ],\n        });\n        if (!ok(savingMessageEndpoint))\n            return savingMessageEndpoint;\n    }\n    async listTools() {\n        if (!this.#messageEndpoint) {\n            return err(`The client wasn't initialized. Call \"connect\" first.`);\n        }\n        const url = this.#messageEndpoint;\n        const id = this.#newId();\n        const path = this.#path();\n        // get list of tools\n        const askToListTools = await fetch({\n            url,\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({\n                jsonrpc: \"2.0\",\n                id,\n                method: \"tools/list\",\n            }),\n        });\n        if (!ok(askToListTools))\n            return askToListTools;\n        const toolList = rpc(await read({ path }));\n        if (!ok(toolList))\n            return toolList;\n        return toolList.tools;\n    }\n    async callTool(name, args) {\n        if (!this.#messageEndpoint) {\n            return err(`The client wasn't initialized. Call \"connect\" first.`);\n        }\n        const url = this.#messageEndpoint;\n        const id = this.#newId();\n        const path = this.#path();\n        // Call tool\n        const askToCallTool = await fetch({\n            url,\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({\n                jsonrpc: \"2.0\",\n                id,\n                method: \"tools/call\",\n                params: { name, arguments: args },\n            }),\n        });\n        const readCallToolResults = rpc(await read({ path }));\n        if (!ok(readCallToolResults))\n            return readCallToolResults;\n        return readCallToolResults.content;\n    }\n    async disconnect() {\n        const path = this.#path();\n        // Close the stream.\n        const deleting = await write({ path, delete: true });\n        if (!ok(deleting))\n            return deleting;\n    }\n}\nasync function mcp(url) {\n    const client = new McpClient(\"connectorid\", url);\n    const connecting = await client.connect();\n    if (!ok(connecting))\n        return connecting;\n    const tools = await client.listTools();\n    if (!ok(tools))\n        return tools;\n    await report({\n        actor: \"MCP Client\",\n        category: \"response\",\n        name: \"List of Tools\",\n        details: { parts: [{ json: tools }] },\n    });\n    const callingTool = await client.callTool(\"browser_navigate\", {\n        url: \"https://google.com\",\n    });\n    if (!ok(callingTool))\n        return callingTool;\n    await report({\n        actor: \"MCP Client\",\n        category: \"response\",\n        name: \"Calling Tool\",\n        details: { parts: [{ json: callingTool }] },\n    });\n    // const disconnecting = await client.disconnect();\n    // if (!ok(disconnecting)) return disconnecting;\n}\nasync function invoke({ endpoint }) {\n    const result = await mcp(endpoint);\n    if (!ok(result))\n        return result;\n    return { result: \"foo\" };\n    // const response = await fetch({\n    //   url: endpoint,\n    //   file: \"/run/saved\",\n    //   stream: \"sse\",\n    // });\n    // if (!ok(response)) return response;\n    // for (;;) {\n    //   const reading = await read({ path: response.response as FileSystemPath });\n    //   console.log(\"READING\", reading);\n    //   if (!ok(reading)) return reading;\n    //   if (\"done\" in reading && reading.done) {\n    //     return { result: \"done\" };\n    //   }\n    //   await report({\n    //     actor: \"Fetch\",\n    //     category: \"Streaming\",\n    //     name: \"Streaming OMG\",\n    //     details: reading.data?.at(-1) || \"none\",\n    //   });\n    // }\n    // return { result: \"done\" };\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                endpoint: {\n                    type: \"string\",\n                    title: \"Endpoint URL\",\n                    default: \"http://127.0.0.1:6277/sse?transportType=sse&url=http%3A%2F%2Flocalhost%3A3001%2Fsse\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                result: {\n                    type: \"object\",\n                    title: \"Result\",\n                },\n            },\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Add a description for your module here.\n */\nvar __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n    return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n};\nvar __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {\n    if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\n    return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\n};\nvar _McpClient_instances, _McpClient_id, _McpClient_messageEndpoint, _McpClient_path, _McpClient_sessionIdPath, _McpClient_newId;\nimport fetch from \"@fetch\";\nimport read from \"@read\";\nimport write from \"@write\";\nimport { ok, err } from \"./a2/utils\";\nimport { report } from \"./a2/output\";\nexport { invoke as default, describe };\nfunction json(result) {\n    if (!ok(result))\n        return result;\n    const j = result.data?.at(-1)?.parts.at(0)?.json;\n    if (!j) {\n        return err(`Invalid result structure`);\n    }\n    return j;\n}\nfunction sse(result) {\n    const e = json(result);\n    if (!ok(e))\n        return e;\n    return e.data;\n}\nfunction rpc(result) {\n    const e = sse(result);\n    if (!ok(e))\n        return e;\n    return e.result;\n}\nclass McpClient {\n    constructor(connectorId, url) {\n        _McpClient_instances.add(this);\n        this.connectorId = connectorId;\n        this.url = url;\n        _McpClient_id.set(this, 0);\n        _McpClient_messageEndpoint.set(this, null);\n    }\n    async connect() {\n        if (__classPrivateFieldGet(this, _McpClient_messageEndpoint, \"f\")) {\n            return err(`Already connected to \"${__classPrivateFieldGet(this, _McpClient_messageEndpoint, \"f\")}\"`);\n        }\n        const file = __classPrivateFieldGet(this, _McpClient_instances, \"m\", _McpClient_path).call(this);\n        const url = this.url;\n        const savedSession = json(await read({ path: __classPrivateFieldGet(this, _McpClient_instances, \"m\", _McpClient_sessionIdPath).call(this) }));\n        if (ok(savedSession)) {\n            console.log(\"EXISTING SESSION\", savedSession);\n            __classPrivateFieldSet(this, _McpClient_messageEndpoint, savedSession.endpoint, \"f\");\n            return;\n        }\n        // Establish connection.\n        const connecting = await fetch({ url, file, stream: \"sse\" });\n        if (!ok(connecting))\n            return connecting;\n        // This is the connection path.\n        const path = connecting.response;\n        // Read the endpoint event\n        const connection = sse(await read({ path }));\n        if (!ok(connection))\n            return connection;\n        // do some crude URL munging.\n        const hostname = url.split(\"/\").slice(0, -1).join(\"/\");\n        const messageEndpoint = `${hostname}${connection}`;\n        const id = __classPrivateFieldGet(this, _McpClient_instances, \"m\", _McpClient_newId).call(this);\n        // send initialize request\n        const initializing = await fetch({\n            url: messageEndpoint,\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({\n                jsonrpc: \"2.0\",\n                id,\n                method: \"initialize\",\n                params: {\n                    protocolVersion: \"2024-11-05\",\n                    clientInfo: {\n                        name: \"Breadboard\",\n                        version: \"1.0.0\",\n                    },\n                    capabilities: {\n                        tools: {},\n                    },\n                },\n            }),\n        });\n        if (!ok(initializing))\n            return initializing;\n        const initializeResponse = rpc(await read({ path }));\n        if (!ok(initializeResponse))\n            return initializeResponse;\n        const confirmInitialization = await fetch({\n            url: messageEndpoint,\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({\n                jsonrpc: \"2.0\",\n                method: \"notifications/initialized\",\n            }),\n        });\n        if (!ok(confirmInitialization))\n            return confirmInitialization;\n        __classPrivateFieldSet(this, _McpClient_messageEndpoint, messageEndpoint, \"f\");\n        // Save message endpoint\n        const savingMessageEndpoint = await write({\n            path: __classPrivateFieldGet(this, _McpClient_instances, \"m\", _McpClient_sessionIdPath).call(this),\n            data: [\n                {\n                    parts: [{ json: { endpoint: messageEndpoint } }],\n                },\n            ],\n        });\n        if (!ok(savingMessageEndpoint))\n            return savingMessageEndpoint;\n    }\n    async listTools() {\n        if (!__classPrivateFieldGet(this, _McpClient_messageEndpoint, \"f\")) {\n            return err(`The client wasn't initialized. Call \"connect\" first.`);\n        }\n        const url = __classPrivateFieldGet(this, _McpClient_messageEndpoint, \"f\");\n        const id = __classPrivateFieldGet(this, _McpClient_instances, \"m\", _McpClient_newId).call(this);\n        const path = __classPrivateFieldGet(this, _McpClient_instances, \"m\", _McpClient_path).call(this);\n        // get list of tools\n        const askToListTools = await fetch({\n            url,\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({\n                jsonrpc: \"2.0\",\n                id,\n                method: \"tools/list\",\n            }),\n        });\n        if (!ok(askToListTools))\n            return askToListTools;\n        const toolList = rpc(await read({ path }));\n        if (!ok(toolList))\n            return toolList;\n        return toolList.tools;\n    }\n    async callTool(name, args) {\n        if (!__classPrivateFieldGet(this, _McpClient_messageEndpoint, \"f\")) {\n            return err(`The client wasn't initialized. Call \"connect\" first.`);\n        }\n        const url = __classPrivateFieldGet(this, _McpClient_messageEndpoint, \"f\");\n        const id = __classPrivateFieldGet(this, _McpClient_instances, \"m\", _McpClient_newId).call(this);\n        const path = __classPrivateFieldGet(this, _McpClient_instances, \"m\", _McpClient_path).call(this);\n        // Call tool\n        const askToCallTool = await fetch({\n            url,\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({\n                jsonrpc: \"2.0\",\n                id,\n                method: \"tools/call\",\n                params: { name, arguments: args },\n            }),\n        });\n        const readCallToolResults = rpc(await read({ path }));\n        if (!ok(readCallToolResults))\n            return readCallToolResults;\n        return readCallToolResults.content;\n    }\n    async disconnect() {\n        const path = __classPrivateFieldGet(this, _McpClient_instances, \"m\", _McpClient_path).call(this);\n        // Close the stream.\n        const deleting = await write({ path, delete: true });\n        if (!ok(deleting))\n            return deleting;\n    }\n}\n_McpClient_id = new WeakMap(), _McpClient_messageEndpoint = new WeakMap(), _McpClient_instances = new WeakSet(), _McpClient_path = function _McpClient_path() {\n    return `/session/mcp/${this.connectorId}/stream`;\n}, _McpClient_sessionIdPath = function _McpClient_sessionIdPath() {\n    return `/session/mcp/${this.connectorId}/id`;\n}, _McpClient_newId = function _McpClient_newId() {\n    var _a;\n    return __classPrivateFieldSet(this, _McpClient_id, (_a = __classPrivateFieldGet(this, _McpClient_id, \"f\"), ++_a), \"f\");\n};\nasync function mcp(url) {\n    const client = new McpClient(\"connectorid\", url);\n    const connecting = await client.connect();\n    if (!ok(connecting))\n        return connecting;\n    const tools = await client.listTools();\n    if (!ok(tools))\n        return tools;\n    await report({\n        actor: \"MCP Client\",\n        category: \"response\",\n        name: \"List of Tools\",\n        details: { parts: [{ json: tools }] },\n    });\n    const callingTool = await client.callTool(\"browser_navigate\", {\n        url: \"https://google.com\",\n    });\n    if (!ok(callingTool))\n        return callingTool;\n    await report({\n        actor: \"MCP Client\",\n        category: \"response\",\n        name: \"Calling Tool\",\n        details: { parts: [{ json: callingTool }] },\n    });\n    // const disconnecting = await client.disconnect();\n    // if (!ok(disconnecting)) return disconnecting;\n}\nasync function invoke({ endpoint }) {\n    const result = await mcp(endpoint);\n    if (!ok(result))\n        return result;\n    return { result: \"foo\" };\n    // const response = await fetch({\n    //   url: endpoint,\n    //   file: \"/run/saved\",\n    //   stream: \"sse\",\n    // });\n    // if (!ok(response)) return response;\n    // for (;;) {\n    //   const reading = await read({ path: response.response as FileSystemPath });\n    //   console.log(\"READING\", reading);\n    //   if (!ok(reading)) return reading;\n    //   if (\"done\" in reading && reading.done) {\n    //     return { result: \"done\" };\n    //   }\n    //   await report({\n    //     actor: \"Fetch\",\n    //     category: \"Streaming\",\n    //     name: \"Streaming OMG\",\n    //     details: reading.data?.at(-1) || \"none\",\n    //   });\n    // }\n    // return { result: \"done\" };\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                endpoint: {\n                    type: \"string\",\n                    title: \"Endpoint URL\",\n                    default: \"http://127.0.0.1:6277/sse?transportType=sse&url=http%3A%2F%2Flocalhost%3A3001%2Fsse\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                result: {\n                    type: \"object\",\n                    title: \"Result\",\n                },\n            },\n        },\n    };\n}\n",
       "metadata": {
         "title": "main",
         "source": {
diff --git a/packages/a2/bgl/generate-text.bgl.json b/packages/a2/bgl/generate-text.bgl.json
index 20883677f5d..617d93bb6c7 100644
--- a/packages/a2/bgl/generate-text.bgl.json
+++ b/packages/a2/bgl/generate-text.bgl.json
@@ -199,7 +199,7 @@
   },
   "modules": {
     "main": {
-      "code": "/**\n * @fileoverview Add a description for your module here.\n */\nimport output from \"@output\";\nimport { createDoneTool, createKeepChattingTool, createKeepChattingResult, } from \"./chat-tools\";\nimport { createSystemInstruction } from \"./system-instruction\";\nimport { report } from \"./a2/output\";\nimport { err, ok, defaultLLMContent, llm } from \"./a2/utils\";\nimport { Template } from \"./a2/template\";\nimport { ArgumentNameGenerator } from \"./a2/introducer\";\nimport { ToolManager } from \"./a2/tool-manager\";\nimport { ListExpander, listPrompt, toList, listSchema } from \"./a2/lists\";\nimport { defaultSafetySettings, } from \"./a2/gemini\";\nimport { GeminiPrompt } from \"./a2/gemini-prompt\";\nexport { invoke as default, describe };\nclass GenerateText {\n    sharedContext;\n    toolManager;\n    doneTool;\n    keepChattingTool;\n    description;\n    context;\n    listMode = false;\n    #hasTools = false;\n    constructor(sharedContext) {\n        this.sharedContext = sharedContext;\n        this.invoke = this.invoke.bind(this);\n    }\n    async initialize() {\n        const { sharedContext } = this;\n        const template = new Template(sharedContext.description);\n        const toolManager = new ToolManager(new ArgumentNameGenerator());\n        const doneTool = createDoneTool();\n        const keepChattingTool = createKeepChattingTool();\n        const substituting = await template.substitute(sharedContext.params, async ({ path: url, instance }) => toolManager.addTool(url, instance));\n        this.#hasTools = toolManager.hasTools();\n        if (sharedContext.chat) {\n            toolManager.addCustomTool(doneTool.name, doneTool.handle());\n            if (!this.#hasTools) {\n                toolManager.addCustomTool(keepChattingTool.name, keepChattingTool.handle());\n            }\n        }\n        if (!ok(substituting)) {\n            return substituting;\n        }\n        this.description = substituting;\n        this.toolManager = toolManager;\n        this.doneTool = doneTool;\n        this.keepChattingTool = keepChattingTool;\n        this.context = [...sharedContext.context, ...sharedContext.work];\n    }\n    createSystemInstruction(makeList) {\n        return createSystemInstruction(this.sharedContext.systemInstruction, makeList);\n    }\n    addKeepChattingResult(context) {\n        context.push(createKeepChattingResult());\n        return context;\n    }\n    /**\n     * Invokes the text generator.\n     * Significant mode flags:\n     * - chat: boolean -- chat mode is on/off\n     * - tools: boolean -- whether or not has tools\n     * - makeList: boolean -- asked to generate a list\n     * - isList: boolean -- is currently in list mode\n     * - model: string -- the model to generate with\n     */\n    async invoke(description, work, isList) {\n        const { sharedContext } = this;\n        const toolManager = this.toolManager;\n        const doneTool = this.doneTool;\n        const keepChattingTool = this.keepChattingTool;\n        // Disallow making nested lists (for now).\n        const makeList = sharedContext.makeList && !isList;\n        let product;\n        const contents = [description, ...work];\n        const safetySettings = defaultSafetySettings();\n        const systemInstruction = this.createSystemInstruction(makeList);\n        const tools = toolManager.list();\n        const inputs = {\n            body: { contents, safetySettings },\n            model: sharedContext.model,\n        };\n        // Unless it's a very first turn, we always supply tools when chatting,\n        // since we add the \"Done\" and \"Keep Chatting\" tools to figure out when\n        // the conversation ends.\n        // In the first turn, we actually create fake \"keep chatting\" result,\n        // to help the LLM get into the rhythm. Like, \"come on, LLM\".\n        const firstTurn = this.firstTurn;\n        const shouldAddTools = (this.chat && !firstTurn) || this.#hasTools;\n        const shouldAddFakeResult = this.chat && firstTurn;\n        if (shouldAddTools) {\n            inputs.body.tools = [...tools];\n            inputs.body.toolConfig = { functionCallingConfig: { mode: \"ANY\" } };\n        }\n        else {\n            if (shouldAddFakeResult) {\n                this.addKeepChattingResult(contents);\n            }\n            inputs.body.systemInstruction = systemInstruction;\n        }\n        // When we have tools, the first call will not try to make a list,\n        // because JSON mode and tool-calling are incompatible.\n        if (makeList && !toolManager.hasTools()) {\n            inputs.body.generationConfig = {\n                responseSchema: listSchema(),\n                responseMimeType: \"application/json\",\n            };\n        }\n        const prompt = new GeminiPrompt(inputs, { toolManager });\n        const result = await prompt.invoke();\n        if (!ok(result))\n            return result;\n        const calledTools = prompt.calledTools || doneTool.invoked || keepChattingTool.invoked;\n        if (calledTools) {\n            if (doneTool.invoked) {\n                return result.last;\n            }\n            const invokedSubgraph = prompt.calledCustomTools;\n            if (invokedSubgraph) {\n                if (makeList && !this.chat) {\n                    // This case might be unusual (making a list of images directly?),\n                    // but handle it for completeness.\n                    // TODO: support this case properly. This seems\n                    const list = toList(result.last);\n                    if (!ok(list))\n                        return list;\n                    product = list;\n                }\n                else {\n                    // Be careful to return subgraph output (which can be media) as-is\n                    // without rewriting/summarizing it with gemini because gemini cannot generate media.\n                    product = result.last;\n                }\n            }\n            else {\n                if (!keepChattingTool.invoked) {\n                    contents.push(...result.all);\n                }\n                const inputs = {\n                    body: { contents, systemInstruction, safetySettings },\n                };\n                if (shouldAddTools) {\n                    // If we added function declarations (or saw a function call request) before, then we need to add them again so\n                    // Gemini isn't confused by the presence of a function call request.\n                    // However, set the mode to NONE so we don't call tools again.\n                    inputs.body.tools = [...tools];\n                    console.log(\"adding tools\");\n                    // Can't set to functionCallingConfig mode to NONE, as that seems to hallucinate tool use.\n                }\n                if (makeList) {\n                    inputs.body.generationConfig = {\n                        responseSchema: listSchema(),\n                        responseMimeType: \"application/json\",\n                    };\n                }\n                console.log(\"afterTools: \", inputs);\n                const afterTools = await new GeminiPrompt(inputs).invoke();\n                if (!ok(afterTools))\n                    return afterTools;\n                if (makeList && !this.chat) {\n                    const list = toList(afterTools.last);\n                    if (!ok(list))\n                        return list;\n                    product = list;\n                }\n                else {\n                    product = afterTools.last;\n                }\n            }\n        }\n        else {\n            if (makeList && !this.chat) {\n                const list = toList(result.last);\n                if (!ok(list))\n                    return list;\n                product = list;\n            }\n            else {\n                product = result.last;\n            }\n        }\n        return product;\n    }\n    get firstTurn() {\n        return this.sharedContext.userInputs.length === 0;\n    }\n    get chat() {\n        // When we are in list mode, disable chat.\n        // Can't have chat inside of a list (yet).\n        return this.sharedContext.chat && !this.listMode;\n    }\n    get doneChatting() {\n        return !!this.doneTool?.invoked;\n    }\n}\nfunction done(result, makeList = false) {\n    if (makeList) {\n        const list = toList(result.at(-1));\n        if (!ok(list))\n            return list;\n        result = [list];\n    }\n    return { done: result };\n}\nasync function keepChatting(sharedContext, result, isList) {\n    const last = result.at(-1);\n    let product = last;\n    if (isList) {\n        const list = toList(last);\n        if (!ok(list))\n            return list;\n        product = list;\n    }\n    await output({\n        schema: {\n            type: \"object\",\n            properties: {\n                \"a-product\": {\n                    type: \"object\",\n                    behavior: [\"llm-content\"],\n                    title: \"Draft\",\n                },\n            },\n        },\n        $metadata: {\n            title: \"Writer\",\n            description: \"Asking user\",\n            icon: \"generative-text\",\n        },\n        \"a-product\": product,\n    });\n    const toInput = {\n        type: \"object\",\n        properties: {\n            request: {\n                type: \"object\",\n                title: \"Please provide feedback\",\n                description: \"Provide feedback or click submit to continue\",\n                behavior: [\"transient\", \"llm-content\"],\n                examples: [defaultLLMContent()],\n            },\n        },\n    };\n    return {\n        toInput,\n        context: {\n            ...sharedContext,\n            work: result,\n            last,\n        },\n    };\n}\nasync function invoke({ context }) {\n    if (!context.description) {\n        const msg = \"No instruction supplied\";\n        await report({\n            actor: \"Text Generator\",\n            name: msg,\n            category: \"Runtime error\",\n            details: `In order to run, I need to have an instruction.`,\n        });\n        return err(msg);\n    }\n    // Check to see if the user ended chat and return early.\n    const { userEndedChat, userInputs, last } = context;\n    if (userEndedChat) {\n        if (!last) {\n            return err(\"Chat ended without any work\");\n        }\n        return done([...context.context, last], context.makeList);\n    }\n    const gen = new GenerateText(context);\n    const initializing = await gen.initialize();\n    if (!ok(initializing))\n        return initializing;\n    const expander = new ListExpander(gen.description, gen.context);\n    expander.expand();\n    gen.listMode = expander.list().length > 1;\n    const result = await expander.map(gen.invoke);\n    if (!ok(result))\n        return result;\n    console.log(\"RESULT\", result);\n    if (gen.doneChatting) {\n        // If done tool was invoked, rewind to remove the last interaction\n        // and return that.\n        const previousResult = context.work.at(-2);\n        if (!previousResult) {\n            return err(`Done chatting, but have nothing to pass along to next step.`);\n        }\n        return done([previousResult], context.makeList);\n    }\n    // Use the gen.chat here, because it will correctly prevent\n    // chat mode when we're in list mode.\n    if (gen.chat && !userEndedChat) {\n        return keepChatting(gen.sharedContext, result, context.makeList);\n    }\n    // Fall through to default response.\n    return done(result);\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                },\n            },\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Add a description for your module here.\n */\nvar __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {\n    if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\n    return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\n};\nvar __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n    return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n};\nvar _GenerateText_hasTools;\nimport output from \"@output\";\nimport { createDoneTool, createKeepChattingTool, createKeepChattingResult, } from \"./chat-tools\";\nimport { createSystemInstruction } from \"./system-instruction\";\nimport { report } from \"./a2/output\";\nimport { err, ok, defaultLLMContent } from \"./a2/utils\";\nimport { Template } from \"./a2/template\";\nimport { ArgumentNameGenerator } from \"./a2/introducer\";\nimport { ToolManager } from \"./a2/tool-manager\";\nimport { ListExpander, toList, listSchema } from \"./a2/lists\";\nimport { defaultSafetySettings, } from \"./a2/gemini\";\nimport { GeminiPrompt } from \"./a2/gemini-prompt\";\nexport { invoke as default, describe };\nclass GenerateText {\n    constructor(sharedContext) {\n        this.sharedContext = sharedContext;\n        this.listMode = false;\n        _GenerateText_hasTools.set(this, false);\n        this.invoke = this.invoke.bind(this);\n    }\n    async initialize() {\n        const { sharedContext } = this;\n        const template = new Template(sharedContext.description);\n        const toolManager = new ToolManager(new ArgumentNameGenerator());\n        const doneTool = createDoneTool();\n        const keepChattingTool = createKeepChattingTool();\n        const substituting = await template.substitute(sharedContext.params, async ({ path: url, instance }) => toolManager.addTool(url, instance));\n        __classPrivateFieldSet(this, _GenerateText_hasTools, toolManager.hasTools(), \"f\");\n        if (sharedContext.chat) {\n            toolManager.addCustomTool(doneTool.name, doneTool.handle());\n            if (!__classPrivateFieldGet(this, _GenerateText_hasTools, \"f\")) {\n                toolManager.addCustomTool(keepChattingTool.name, keepChattingTool.handle());\n            }\n        }\n        if (!ok(substituting)) {\n            return substituting;\n        }\n        this.description = substituting;\n        this.toolManager = toolManager;\n        this.doneTool = doneTool;\n        this.keepChattingTool = keepChattingTool;\n        this.context = [...sharedContext.context, ...sharedContext.work];\n    }\n    createSystemInstruction(makeList) {\n        return createSystemInstruction(this.sharedContext.systemInstruction, makeList);\n    }\n    addKeepChattingResult(context) {\n        context.push(createKeepChattingResult());\n        return context;\n    }\n    /**\n     * Invokes the text generator.\n     * Significant mode flags:\n     * - chat: boolean -- chat mode is on/off\n     * - tools: boolean -- whether or not has tools\n     * - makeList: boolean -- asked to generate a list\n     * - isList: boolean -- is currently in list mode\n     * - model: string -- the model to generate with\n     */\n    async invoke(description, work, isList) {\n        const { sharedContext } = this;\n        const toolManager = this.toolManager;\n        const doneTool = this.doneTool;\n        const keepChattingTool = this.keepChattingTool;\n        // Disallow making nested lists (for now).\n        const makeList = sharedContext.makeList && !isList;\n        let product;\n        const contents = [description, ...work];\n        const safetySettings = defaultSafetySettings();\n        const systemInstruction = this.createSystemInstruction(makeList);\n        const tools = toolManager.list();\n        const inputs = {\n            body: { contents, safetySettings },\n            model: sharedContext.model,\n        };\n        // Unless it's a very first turn, we always supply tools when chatting,\n        // since we add the \"Done\" and \"Keep Chatting\" tools to figure out when\n        // the conversation ends.\n        // In the first turn, we actually create fake \"keep chatting\" result,\n        // to help the LLM get into the rhythm. Like, \"come on, LLM\".\n        const firstTurn = this.firstTurn;\n        const shouldAddTools = (this.chat && !firstTurn) || __classPrivateFieldGet(this, _GenerateText_hasTools, \"f\");\n        const shouldAddFakeResult = this.chat && firstTurn;\n        if (shouldAddTools) {\n            inputs.body.tools = [...tools];\n            inputs.body.toolConfig = { functionCallingConfig: { mode: \"ANY\" } };\n        }\n        else {\n            if (shouldAddFakeResult) {\n                this.addKeepChattingResult(contents);\n            }\n            inputs.body.systemInstruction = systemInstruction;\n        }\n        // When we have tools, the first call will not try to make a list,\n        // because JSON mode and tool-calling are incompatible.\n        if (makeList && !toolManager.hasTools()) {\n            inputs.body.generationConfig = {\n                responseSchema: listSchema(),\n                responseMimeType: \"application/json\",\n            };\n        }\n        const prompt = new GeminiPrompt(inputs, { toolManager });\n        const result = await prompt.invoke();\n        if (!ok(result))\n            return result;\n        const calledTools = prompt.calledTools || doneTool.invoked || keepChattingTool.invoked;\n        if (calledTools) {\n            if (doneTool.invoked) {\n                return result.last;\n            }\n            const invokedSubgraph = prompt.calledCustomTools;\n            if (invokedSubgraph) {\n                if (makeList && !this.chat) {\n                    // This case might be unusual (making a list of images directly?),\n                    // but handle it for completeness.\n                    // TODO: support this case properly. This seems\n                    const list = toList(result.last);\n                    if (!ok(list))\n                        return list;\n                    product = list;\n                }\n                else {\n                    // Be careful to return subgraph output (which can be media) as-is\n                    // without rewriting/summarizing it with gemini because gemini cannot generate media.\n                    product = result.last;\n                }\n            }\n            else {\n                if (!keepChattingTool.invoked) {\n                    contents.push(...result.all);\n                }\n                const inputs = {\n                    body: { contents, systemInstruction, safetySettings },\n                };\n                if (shouldAddTools) {\n                    // If we added function declarations (or saw a function call request) before, then we need to add them again so\n                    // Gemini isn't confused by the presence of a function call request.\n                    // However, set the mode to NONE so we don't call tools again.\n                    inputs.body.tools = [...tools];\n                    console.log(\"adding tools\");\n                    // Can't set to functionCallingConfig mode to NONE, as that seems to hallucinate tool use.\n                }\n                if (makeList) {\n                    inputs.body.generationConfig = {\n                        responseSchema: listSchema(),\n                        responseMimeType: \"application/json\",\n                    };\n                }\n                console.log(\"afterTools: \", inputs);\n                const afterTools = await new GeminiPrompt(inputs).invoke();\n                if (!ok(afterTools))\n                    return afterTools;\n                if (makeList && !this.chat) {\n                    const list = toList(afterTools.last);\n                    if (!ok(list))\n                        return list;\n                    product = list;\n                }\n                else {\n                    product = afterTools.last;\n                }\n            }\n        }\n        else {\n            if (makeList && !this.chat) {\n                const list = toList(result.last);\n                if (!ok(list))\n                    return list;\n                product = list;\n            }\n            else {\n                product = result.last;\n            }\n        }\n        return product;\n    }\n    get firstTurn() {\n        return this.sharedContext.userInputs.length === 0;\n    }\n    get chat() {\n        // When we are in list mode, disable chat.\n        // Can't have chat inside of a list (yet).\n        return this.sharedContext.chat && !this.listMode;\n    }\n    get doneChatting() {\n        return !!this.doneTool?.invoked;\n    }\n}\n_GenerateText_hasTools = new WeakMap();\nfunction done(result, makeList = false) {\n    if (makeList) {\n        const list = toList(result.at(-1));\n        if (!ok(list))\n            return list;\n        result = [list];\n    }\n    return { done: result };\n}\nasync function keepChatting(sharedContext, result, isList) {\n    const last = result.at(-1);\n    let product = last;\n    if (isList) {\n        const list = toList(last);\n        if (!ok(list))\n            return list;\n        product = list;\n    }\n    await output({\n        schema: {\n            type: \"object\",\n            properties: {\n                \"a-product\": {\n                    type: \"object\",\n                    behavior: [\"llm-content\"],\n                    title: \"Draft\",\n                },\n            },\n        },\n        $metadata: {\n            title: \"Writer\",\n            description: \"Asking user\",\n            icon: \"generative-text\",\n        },\n        \"a-product\": product,\n    });\n    const toInput = {\n        type: \"object\",\n        properties: {\n            request: {\n                type: \"object\",\n                title: \"Please provide feedback\",\n                description: \"Provide feedback or click submit to continue\",\n                behavior: [\"transient\", \"llm-content\"],\n                examples: [defaultLLMContent()],\n            },\n        },\n    };\n    return {\n        toInput,\n        context: {\n            ...sharedContext,\n            work: result,\n            last,\n        },\n    };\n}\nasync function invoke({ context }) {\n    if (!context.description) {\n        const msg = \"No instruction supplied\";\n        await report({\n            actor: \"Text Generator\",\n            name: msg,\n            category: \"Runtime error\",\n            details: `In order to run, I need to have an instruction.`,\n        });\n        return err(msg);\n    }\n    // Check to see if the user ended chat and return early.\n    const { userEndedChat, userInputs, last } = context;\n    if (userEndedChat) {\n        if (!last) {\n            return err(\"Chat ended without any work\");\n        }\n        return done([...context.context, last], context.makeList);\n    }\n    const gen = new GenerateText(context);\n    const initializing = await gen.initialize();\n    if (!ok(initializing))\n        return initializing;\n    const expander = new ListExpander(gen.description, gen.context);\n    expander.expand();\n    gen.listMode = expander.list().length > 1;\n    const result = await expander.map(gen.invoke);\n    if (!ok(result))\n        return result;\n    console.log(\"RESULT\", result);\n    if (gen.doneChatting) {\n        // If done tool was invoked, rewind to remove the last interaction\n        // and return that.\n        const previousResult = context.work.at(-2);\n        if (!previousResult) {\n            return err(`Done chatting, but have nothing to pass along to next step.`);\n        }\n        return done([previousResult], context.makeList);\n    }\n    // Use the gen.chat here, because it will correctly prevent\n    // chat mode when we're in list mode.\n    if (gen.chat && !userEndedChat) {\n        return keepChatting(gen.sharedContext, result, context.makeList);\n    }\n    // Fall through to default response.\n    return done(result);\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                },\n            },\n        },\n    };\n}\n",
       "metadata": {
         "title": "main",
         "source": {
@@ -211,7 +211,7 @@
       }
     },
     "join": {
-      "code": "/**\n * @fileoverview Joins user input and Agent Context\n */\nimport {} from \"./a2/common\";\nimport { isEmpty } from \"./a2/utils\";\nimport { addContent } from \"./a2/lists\";\nexport { invoke as default, describe };\nasync function invoke({ context, request }) {\n    context.userEndedChat = isEmpty(request);\n    context.userInputs.push(request);\n    if (!context.userEndedChat) {\n        context.work = addContent(context.work, request);\n    }\n    return { context };\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    title: \"Agent Context\",\n                    type: \"object\",\n                },\n                request: {\n                    title: \"User Input\",\n                    type: \"object\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    title: \"Agent Context\",\n                    type: \"object\",\n                },\n            },\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Joins user input and Agent Context\n */\nimport { isEmpty } from \"./a2/utils\";\nimport { addContent } from \"./a2/lists\";\nexport { invoke as default, describe };\nasync function invoke({ context, request }) {\n    context.userEndedChat = isEmpty(request);\n    context.userInputs.push(request);\n    if (!context.userEndedChat) {\n        context.work = addContent(context.work, request);\n    }\n    return { context };\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    title: \"Agent Context\",\n                    type: \"object\",\n                },\n                request: {\n                    title: \"User Input\",\n                    type: \"object\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    title: \"Agent Context\",\n                    type: \"object\",\n                },\n            },\n        },\n    };\n}\n",
       "metadata": {
         "title": "join",
         "source": {
@@ -223,7 +223,7 @@
       }
     },
     "entry": {
-      "code": "/**\n * @fileoverview Manages the entry point: describer, passing the inputs, etc.\n */\nimport {} from \"./a2/common\";\nimport { ok, toLLMContent, defaultLLMContent } from \"./a2/utils\";\nimport { Template } from \"./a2/template\";\nimport { readSettings } from \"./a2/settings\";\nimport { defaultSystemInstruction } from \"./system-instruction\";\nexport { invoke as default, describe };\nasync function invoke({ context, \"p-chat\": chat, \"p-list\": makeList, \"b-system-instruction\": systemInstruction, \"p-model-name\": model = \"\", description, ...params }) {\n    // Make sure it's a boolean.\n    chat = !!chat;\n    context ??= [];\n    const type = \"work\";\n    return {\n        context: {\n            id: Math.random().toString(36).substring(2, 5),\n            chat,\n            makeList,\n            listPath: [],\n            context,\n            userInputs: [],\n            defaultModel: model,\n            model: model,\n            description,\n            type,\n            work: [],\n            userEndedChat: false,\n            params,\n            systemInstruction,\n        },\n    };\n}\nasync function describe({ inputs: { description, \"config$ask-user\": chat }, }) {\n    const settings = await readSettings();\n    const chatSchema = chat ? [\"hint-chat-mode\"] : [];\n    const experimental = ok(settings) && !!settings[\"Show Experimental Components\"];\n    const template = new Template(description);\n    let extra = {};\n    if (experimental) {\n        extra = {\n            \"p-list\": {\n                type: \"boolean\",\n                title: \"Make a list\",\n                behavior: [\"config\", \"hint-preview\"],\n                icon: \"summarize\",\n                description: \"When checked, this step will try to create a list as its output. Make sure that the prompt asks for a list of some sort\",\n            },\n        };\n    }\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                description: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-preview\"],\n                    title: \"Prompt\",\n                    description: \"Give the model additional context on what to do, like specific rules/guidelines to adhere to or specify behavior separate from the provided context.\",\n                    default: defaultLLMContent(),\n                },\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                \"p-chat\": {\n                    type: \"boolean\",\n                    title: \"Refine based on user input\",\n                    behavior: [\"config\", \"hint-preview\"],\n                    icon: \"chat\",\n                    description: \"When checked, this step will chat with the user, asking to review work, requesting additional information, etc.\",\n                },\n                \"b-system-instruction\": {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-advanced\"],\n                    title: \"System Instruction\",\n                    description: \"The system instruction for the model\",\n                    default: JSON.stringify(defaultSystemInstruction()),\n                },\n                \"p-model-name\": {\n                    type: \"string\",\n                    behavior: [\"llm-content\"],\n                    title: \"Model\",\n                    description: \"The specific model version to generate with\",\n                },\n                ...extra,\n                ...template.schemas(),\n            },\n            behavior: [\"at-wireable\", ...chatSchema],\n            ...template.requireds(),\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"main-port\", \"hint-text\"],\n                },\n            },\n        },\n        title: \"Make Text\",\n        metadata: {\n            icon: \"generative-text\",\n            tags: [\"quick-access\", \"generative\"],\n            order: 1,\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Manages the entry point: describer, passing the inputs, etc.\n */\nimport { ok, defaultLLMContent } from \"./a2/utils\";\nimport { Template } from \"./a2/template\";\nimport { readSettings } from \"./a2/settings\";\nimport { defaultSystemInstruction } from \"./system-instruction\";\nexport { invoke as default, describe };\nasync function invoke({ context, \"p-chat\": chat, \"p-list\": makeList, \"b-system-instruction\": systemInstruction, \"p-model-name\": model = \"\", description, ...params }) {\n    // Make sure it's a boolean.\n    chat = !!chat;\n    context ?? (context = []);\n    const type = \"work\";\n    return {\n        context: {\n            id: Math.random().toString(36).substring(2, 5),\n            chat,\n            makeList,\n            listPath: [],\n            context,\n            userInputs: [],\n            defaultModel: model,\n            model: model,\n            description,\n            type,\n            work: [],\n            userEndedChat: false,\n            params,\n            systemInstruction,\n        },\n    };\n}\nasync function describe({ inputs: { description, \"config$ask-user\": chat }, }) {\n    const settings = await readSettings();\n    const chatSchema = chat ? [\"hint-chat-mode\"] : [];\n    const experimental = ok(settings) && !!settings[\"Show Experimental Components\"];\n    const template = new Template(description);\n    let extra = {};\n    if (experimental) {\n        extra = {\n            \"p-list\": {\n                type: \"boolean\",\n                title: \"Make a list\",\n                behavior: [\"config\", \"hint-preview\"],\n                icon: \"summarize\",\n                description: \"When checked, this step will try to create a list as its output. Make sure that the prompt asks for a list of some sort\",\n            },\n        };\n    }\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                description: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-preview\"],\n                    title: \"Prompt\",\n                    description: \"Give the model additional context on what to do, like specific rules/guidelines to adhere to or specify behavior separate from the provided context.\",\n                    default: defaultLLMContent(),\n                },\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                \"p-chat\": {\n                    type: \"boolean\",\n                    title: \"Refine based on user input\",\n                    behavior: [\"config\", \"hint-preview\"],\n                    icon: \"chat\",\n                    description: \"When checked, this step will chat with the user, asking to review work, requesting additional information, etc.\",\n                },\n                \"b-system-instruction\": {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-advanced\"],\n                    title: \"System Instruction\",\n                    description: \"The system instruction for the model\",\n                    default: JSON.stringify(defaultSystemInstruction()),\n                },\n                \"p-model-name\": {\n                    type: \"string\",\n                    behavior: [\"llm-content\"],\n                    title: \"Model\",\n                    description: \"The specific model version to generate with\",\n                },\n                ...extra,\n                ...template.schemas(),\n            },\n            behavior: [\"at-wireable\", ...chatSchema],\n            ...template.requireds(),\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"main-port\", \"hint-text\"],\n                },\n            },\n        },\n        title: \"Make Text\",\n        metadata: {\n            icon: \"generative-text\",\n            tags: [\"quick-access\", \"generative\"],\n            order: 1,\n        },\n    };\n}\n",
       "metadata": {
         "title": "Make Text",
         "source": {
@@ -235,7 +235,7 @@
       }
     },
     "types": {
-      "code": "/**\n * @fileoverview Common type definitions\n */\n",
+      "code": "/**\n * @fileoverview Common type definitions\n */\nexport {};\n",
       "metadata": {
         "title": "types",
         "source": {
@@ -247,7 +247,7 @@
       }
     },
     "chat-tools": {
-      "code": "/**\n * @fileoverview Tools for conversational (\"chat\") mode\n */\nexport { createDoneTool, createKeepChattingTool, createKeepChattingResult };\nfunction createKeepChattingResult() {\n    return {\n        parts: [{ functionCall: { name: \"User_Asks_For_More_Work\", args: {} } }],\n        role: \"model\",\n    };\n}\nclass ChatToolImpl {\n    name;\n    description;\n    #invoked = false;\n    constructor(name, description) {\n        this.name = name;\n        this.description = description;\n    }\n    reset() {\n        this.#invoked = false;\n    }\n    get invoked() {\n        return this.#invoked;\n    }\n    declaration() {\n        return {\n            name: this.name,\n            description: this.description,\n        };\n    }\n    handle() {\n        return {\n            tool: this.declaration(),\n            url: \"\",\n            passContext: false,\n            invoke: async () => {\n                this.#invoked = true;\n            },\n        };\n    }\n}\nfunction createDoneTool() {\n    return new ChatToolImpl(\"User_Says_Done\", \"Call when the user indicates they are done with the conversation and are ready to move on\");\n}\nfunction createKeepChattingTool() {\n    return new ChatToolImpl(\"User_Asks_For_More_Work\", \"Call when the user asked a question or issued instruction to do more work\");\n}\n",
+      "code": "/**\n * @fileoverview Tools for conversational (\"chat\") mode\n */\nvar __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {\n    if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\n    return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\n};\nvar __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n    return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n};\nvar _ChatToolImpl_invoked;\nexport { createDoneTool, createKeepChattingTool, createKeepChattingResult };\nfunction createKeepChattingResult() {\n    return {\n        parts: [{ functionCall: { name: \"User_Asks_For_More_Work\", args: {} } }],\n        role: \"model\",\n    };\n}\nclass ChatToolImpl {\n    constructor(name, description) {\n        this.name = name;\n        this.description = description;\n        _ChatToolImpl_invoked.set(this, false);\n    }\n    reset() {\n        __classPrivateFieldSet(this, _ChatToolImpl_invoked, false, \"f\");\n    }\n    get invoked() {\n        return __classPrivateFieldGet(this, _ChatToolImpl_invoked, \"f\");\n    }\n    declaration() {\n        return {\n            name: this.name,\n            description: this.description,\n        };\n    }\n    handle() {\n        return {\n            tool: this.declaration(),\n            url: \"\",\n            passContext: false,\n            invoke: async () => {\n                __classPrivateFieldSet(this, _ChatToolImpl_invoked, true, \"f\");\n            },\n        };\n    }\n}\n_ChatToolImpl_invoked = new WeakMap();\nfunction createDoneTool() {\n    return new ChatToolImpl(\"User_Says_Done\", \"Call when the user indicates they are done with the conversation and are ready to move on\");\n}\nfunction createKeepChattingTool() {\n    return new ChatToolImpl(\"User_Asks_For_More_Work\", \"Call when the user asked a question or issued instruction to do more work\");\n}\n",
       "metadata": {
         "title": "chat-tools",
         "source": {
diff --git a/packages/a2/bgl/generate.bgl.json b/packages/a2/bgl/generate.bgl.json
index 07cf6768732..a5f3df473cb 100644
--- a/packages/a2/bgl/generate.bgl.json
+++ b/packages/a2/bgl/generate.bgl.json
@@ -57,7 +57,7 @@
   },
   "modules": {
     "main": {
-      "code": "/**\n * @fileoverview Mega step for generation capabilities.\n */\nimport describeGraph from \"@describe\";\nimport invokeGraph from \"@invoke\";\nimport { ok } from \"./a2/utils\";\nimport {} from \"./a2/common\";\nexport { invoke as default, describe };\nconst MODES = [\n    {\n        id: \"text\",\n        type: \"text\",\n        url: \"embed://a2/generate-text.bgl.json#daf082ca-c1aa-4aff-b2c8-abeb984ab66c\",\n        title: \"Gemini 2.0 Flash\",\n        description: \"For everyday tasks, plus more\",\n        icon: \"text_analysis\",\n        modelName: \"gemini-2.0-flash\",\n    },\n    {\n        id: \"text-2.5-flash\",\n        type: \"text\",\n        url: \"embed://a2/generate-text.bgl.json#daf082ca-c1aa-4aff-b2c8-abeb984ab66c\",\n        title: \"Gemini 2.5 Flash [Experimental]\",\n        description: \"Uses advanced reasoning\",\n        icon: \"text_analysis\",\n        modelName: \"gemini-2.5-flash-preview-04-17\",\n    },\n    {\n        id: \"text-2.5-pro\",\n        type: \"text\",\n        url: \"embed://a2/generate-text.bgl.json#daf082ca-c1aa-4aff-b2c8-abeb984ab66c\",\n        title: \"Gemini 2.5 Pro [Experimental]\",\n        description: \"Best for complex tasks\",\n        icon: \"text_analysis\",\n        modelName: \"gemini-2.5-pro-preview-03-25\",\n    },\n    {\n        id: \"think\",\n        type: \"think\",\n        url: \"embed://a2/go-over-list.bgl.json#module:main\",\n        title: \"Plan and Execute with Gemini 2.0 Flash\",\n        description: \"Plans and executes complex tasks\",\n        icon: \"spark\",\n        modelName: \"gemini-2.0-flash\",\n    },\n    {\n        id: \"image-gen\",\n        type: \"image-gen\",\n        url: \"embed://a2/a2.bgl.json#module:image-generator\",\n        title: \"Imagen 3\",\n        description: \"Generates images from text\",\n        icon: \"photo_spark\",\n    },\n    {\n        id: \"image\",\n        type: \"image\",\n        url: \"embed://a2/a2.bgl.json#module:image-editor\",\n        title: \"Gemini 2.0 Flash: Image Generation\",\n        description: \"Generates images from text and images\",\n        icon: \"photo_spark\",\n    },\n    {\n        id: \"audio\",\n        type: \"audio\",\n        url: \"embed://a2/audio-generator.bgl.json#module:main\",\n        title: \"AudioLM\",\n        description: \"Generates speech from text\",\n        icon: \"audio_magic_eraser\",\n    },\n    {\n        id: \"video\",\n        type: \"video\",\n        url: \"embed://a2/video-generator.bgl.json#module:main\",\n        title: \"Veo 2\",\n        description: \"Generates videos from text and images\",\n        icon: \"videocam_auto\",\n    },\n];\nconst DEFAULT_MODE = MODES[0];\nconst modeMap = new Map(MODES.map((mode) => [mode.id, mode]));\nconst PROMPT_PORT = \"config$prompt\";\nconst ASK_USER_PORT = \"config$ask-user\";\nconst LIST_PORT = \"config$list\";\nconst MODEL_PORT = \"modelName\";\n// Maps the prompt port to various names of the other ports.\nconst portMapForward = new Map([\n    [\n        MODES[0].id,\n        new Map([\n            [PROMPT_PORT, \"description\"],\n            [ASK_USER_PORT, \"p-chat\"],\n            [LIST_PORT, \"p-list\"],\n        ]),\n    ],\n    [\n        MODES[1].id,\n        new Map([\n            [PROMPT_PORT, \"description\"],\n            [ASK_USER_PORT, \"p-chat\"],\n            [LIST_PORT, \"p-list\"],\n        ]),\n    ],\n    [\n        MODES[2].id,\n        new Map([\n            [PROMPT_PORT, \"description\"],\n            [ASK_USER_PORT, \"p-chat\"],\n            [LIST_PORT, \"p-list\"],\n        ]),\n    ],\n    [\n        MODES[3].id,\n        new Map([\n            [PROMPT_PORT, \"plan\"],\n            [LIST_PORT, \"z-list\"],\n        ]),\n    ],\n    [MODES[4].id, new Map([[PROMPT_PORT, \"instruction\"]])],\n    [MODES[5].id, new Map([[PROMPT_PORT, \"instruction\"]])],\n    [MODES[6].id, new Map([[PROMPT_PORT, \"text\"]])],\n    [MODES[7].id, new Map([[PROMPT_PORT, \"instruction\"]])],\n]);\nconst portMapReverse = new Map(Array.from(portMapForward.entries()).map(([mode, map]) => {\n    const inverted = new Map();\n    for (const [from, to] of map) {\n        inverted.set(to, from);\n    }\n    return [mode, inverted];\n}));\nfunction translate(ports, map) {\n    return Object.fromEntries(Object.entries(ports).map(([name, value]) => [map.get(name) || name, value]));\n}\nfunction forwardPorts(mode, ports) {\n    const forwardingMap = portMapForward.get(mode);\n    if (!forwardingMap)\n        return ports;\n    return translate(ports, forwardingMap);\n}\nfunction receivePorts(mode, ports) {\n    const reverseMap = portMapReverse.get(mode);\n    if (!reverseMap)\n        return ports;\n    return translate(ports, reverseMap);\n}\nfunction getMode(modeId) {\n    return modeMap.get(modeId || DEFAULT_MODE.id) || DEFAULT_MODE;\n}\nasync function invoke({ \"generation-mode\": mode, ...rest }) {\n    const { url: $board, type, modelName } = getMode(mode);\n    // Model is treated as part of the Mode, but actually maps N:1\n    // on actual underlying step type.\n    if (modelName) {\n        console.log(`Generating with ${modelName}`);\n        rest[\"p-model-name\"] = modelName;\n    }\n    return await invokeGraph({ $board, ...forwardPorts(type, rest) });\n}\nasync function describe({ inputs: { \"generation-mode\": mode, ...rest }, asType, }) {\n    const metadata = {\n        title: \"Generate\",\n        description: \"Uses Gemini to generate content and call tools\",\n        metadata: {\n            icon: \"generative\",\n            tags: [\"quick-access\", \"generative\", \"generate\"],\n            order: 1,\n        },\n    };\n    // When asked for to be described as type, skip trying to\n    // get the detailed schema and just return metadata.\n    if (asType) {\n        return {\n            ...metadata,\n            inputSchema: {},\n            outputSchema: {},\n        };\n    }\n    const { url, type } = getMode(mode);\n    const describing = await describeGraph({ url, inputs: rest });\n    let behavior = [];\n    let modeSchema = {};\n    if (ok(describing)) {\n        modeSchema = receivePorts(type, describing.inputSchema.properties || modeSchema);\n        behavior = describing.inputSchema.behavior || [];\n    }\n    return {\n        title: \"Generate\",\n        description: \"Uses Gemini to generate content and call tools\",\n        metadata: {\n            icon: \"generative\",\n            tags: [\"quick-access\", \"generative\", \"generate\"],\n            order: 1,\n        },\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                \"generation-mode\": {\n                    type: \"string\",\n                    title: \"Mode\",\n                    enum: MODES,\n                    behavior: [\"config\", \"hint-preview\", \"reactive\", \"hint-controller\"],\n                },\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                ...modeSchema,\n            },\n            behavior,\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"main-port\"],\n                },\n            },\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Mega step for generation capabilities.\n */\nimport describeGraph from \"@describe\";\nimport invokeGraph from \"@invoke\";\nimport { ok } from \"./a2/utils\";\nexport { invoke as default, describe };\nconst MODES = [\n    {\n        id: \"text\",\n        type: \"text\",\n        url: \"embed://a2/generate-text.bgl.json#daf082ca-c1aa-4aff-b2c8-abeb984ab66c\",\n        title: \"Gemini 2.0 Flash\",\n        description: \"For everyday tasks, plus more\",\n        icon: \"text_analysis\",\n        modelName: \"gemini-2.0-flash\",\n    },\n    {\n        id: \"text-2.5-flash\",\n        type: \"text\",\n        url: \"embed://a2/generate-text.bgl.json#daf082ca-c1aa-4aff-b2c8-abeb984ab66c\",\n        title: \"Gemini 2.5 Flash [Experimental]\",\n        description: \"Uses advanced reasoning\",\n        icon: \"text_analysis\",\n        modelName: \"gemini-2.5-flash-preview-04-17\",\n    },\n    {\n        id: \"text-2.5-pro\",\n        type: \"text\",\n        url: \"embed://a2/generate-text.bgl.json#daf082ca-c1aa-4aff-b2c8-abeb984ab66c\",\n        title: \"Gemini 2.5 Pro [Experimental]\",\n        description: \"Best for complex tasks\",\n        icon: \"text_analysis\",\n        modelName: \"gemini-2.5-pro-preview-03-25\",\n    },\n    {\n        id: \"think\",\n        type: \"think\",\n        url: \"embed://a2/go-over-list.bgl.json#module:main\",\n        title: \"Plan and Execute with Gemini 2.0 Flash\",\n        description: \"Plans and executes complex tasks\",\n        icon: \"spark\",\n        modelName: \"gemini-2.0-flash\",\n    },\n    {\n        id: \"image-gen\",\n        type: \"image-gen\",\n        url: \"embed://a2/a2.bgl.json#module:image-generator\",\n        title: \"Imagen 3\",\n        description: \"Generates images from text\",\n        icon: \"photo_spark\",\n    },\n    {\n        id: \"image\",\n        type: \"image\",\n        url: \"embed://a2/a2.bgl.json#module:image-editor\",\n        title: \"Gemini 2.0 Flash: Image Generation\",\n        description: \"Generates images from text and images\",\n        icon: \"photo_spark\",\n    },\n    {\n        id: \"audio\",\n        type: \"audio\",\n        url: \"embed://a2/audio-generator.bgl.json#module:main\",\n        title: \"AudioLM\",\n        description: \"Generates speech from text\",\n        icon: \"audio_magic_eraser\",\n    },\n    {\n        id: \"video\",\n        type: \"video\",\n        url: \"embed://a2/video-generator.bgl.json#module:main\",\n        title: \"Veo 2\",\n        description: \"Generates videos from text and images\",\n        icon: \"videocam_auto\",\n    },\n];\nconst DEFAULT_MODE = MODES[0];\nconst modeMap = new Map(MODES.map((mode) => [mode.id, mode]));\nconst PROMPT_PORT = \"config$prompt\";\nconst ASK_USER_PORT = \"config$ask-user\";\nconst LIST_PORT = \"config$list\";\nconst MODEL_PORT = \"modelName\";\n// Maps the prompt port to various names of the other ports.\nconst portMapForward = new Map([\n    [\n        MODES[0].id,\n        new Map([\n            [PROMPT_PORT, \"description\"],\n            [ASK_USER_PORT, \"p-chat\"],\n            [LIST_PORT, \"p-list\"],\n        ]),\n    ],\n    [\n        MODES[1].id,\n        new Map([\n            [PROMPT_PORT, \"description\"],\n            [ASK_USER_PORT, \"p-chat\"],\n            [LIST_PORT, \"p-list\"],\n        ]),\n    ],\n    [\n        MODES[2].id,\n        new Map([\n            [PROMPT_PORT, \"description\"],\n            [ASK_USER_PORT, \"p-chat\"],\n            [LIST_PORT, \"p-list\"],\n        ]),\n    ],\n    [\n        MODES[3].id,\n        new Map([\n            [PROMPT_PORT, \"plan\"],\n            [LIST_PORT, \"z-list\"],\n        ]),\n    ],\n    [MODES[4].id, new Map([[PROMPT_PORT, \"instruction\"]])],\n    [MODES[5].id, new Map([[PROMPT_PORT, \"instruction\"]])],\n    [MODES[6].id, new Map([[PROMPT_PORT, \"text\"]])],\n    [MODES[7].id, new Map([[PROMPT_PORT, \"instruction\"]])],\n]);\nconst portMapReverse = new Map(Array.from(portMapForward.entries()).map(([mode, map]) => {\n    const inverted = new Map();\n    for (const [from, to] of map) {\n        inverted.set(to, from);\n    }\n    return [mode, inverted];\n}));\nfunction translate(ports, map) {\n    return Object.fromEntries(Object.entries(ports).map(([name, value]) => [map.get(name) || name, value]));\n}\nfunction forwardPorts(mode, ports) {\n    const forwardingMap = portMapForward.get(mode);\n    if (!forwardingMap)\n        return ports;\n    return translate(ports, forwardingMap);\n}\nfunction receivePorts(mode, ports) {\n    const reverseMap = portMapReverse.get(mode);\n    if (!reverseMap)\n        return ports;\n    return translate(ports, reverseMap);\n}\nfunction getMode(modeId) {\n    return modeMap.get(modeId || DEFAULT_MODE.id) || DEFAULT_MODE;\n}\nasync function invoke({ \"generation-mode\": mode, ...rest }) {\n    const { url: $board, type, modelName } = getMode(mode);\n    // Model is treated as part of the Mode, but actually maps N:1\n    // on actual underlying step type.\n    if (modelName) {\n        console.log(`Generating with ${modelName}`);\n        rest[\"p-model-name\"] = modelName;\n    }\n    return await invokeGraph({ $board, ...forwardPorts(type, rest) });\n}\nasync function describe({ inputs: { \"generation-mode\": mode, ...rest }, asType, }) {\n    const metadata = {\n        title: \"Generate\",\n        description: \"Uses Gemini to generate content and call tools\",\n        metadata: {\n            icon: \"generative\",\n            tags: [\"quick-access\", \"generative\", \"generate\"],\n            order: 1,\n        },\n    };\n    // When asked for to be described as type, skip trying to\n    // get the detailed schema and just return metadata.\n    if (asType) {\n        return {\n            ...metadata,\n            inputSchema: {},\n            outputSchema: {},\n        };\n    }\n    const { url, type } = getMode(mode);\n    const describing = await describeGraph({ url, inputs: rest });\n    let behavior = [];\n    let modeSchema = {};\n    if (ok(describing)) {\n        modeSchema = receivePorts(type, describing.inputSchema.properties || modeSchema);\n        behavior = describing.inputSchema.behavior || [];\n    }\n    return {\n        title: \"Generate\",\n        description: \"Uses Gemini to generate content and call tools\",\n        metadata: {\n            icon: \"generative\",\n            tags: [\"quick-access\", \"generative\", \"generate\"],\n            order: 1,\n        },\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                \"generation-mode\": {\n                    type: \"string\",\n                    title: \"Mode\",\n                    enum: MODES,\n                    behavior: [\"config\", \"hint-preview\", \"reactive\", \"hint-controller\"],\n                },\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                ...modeSchema,\n            },\n            behavior,\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"main-port\"],\n                },\n            },\n        },\n    };\n}\n",
       "metadata": {
         "title": "main",
         "source": {
diff --git a/packages/a2/bgl/gmail.bgl.json b/packages/a2/bgl/gmail.bgl.json
index 50454a4590f..7ffbec52b35 100644
--- a/packages/a2/bgl/gmail.bgl.json
+++ b/packages/a2/bgl/gmail.bgl.json
@@ -30,7 +30,12 @@
       }
     },
     "userModified": true,
-    "tags": ["connector", "connector-singleton", "experimental", "published"],
+    "tags": [
+      "connector",
+      "connector-singleton",
+      "experimental",
+      "published"
+    ],
     "comments": [
       {
         "id": "comment-c74afa15",
@@ -49,7 +54,7 @@
   },
   "modules": {
     "configurator": {
-      "code": "/**\n * @fileoverview Add a description for your module here.\n */\nimport { err, ok } from \"./a2/utils\";\nimport { createConfigurator } from \"./a2/connector-manager\";\nimport read from \"@read\";\nimport write from \"@write\";\nexport { invoke as default, describe };\nconst { invoke, describe } = createConfigurator({\n    title: \"GMail\",\n    initialize: async () => {\n        return { title: \"GMail\", configuration: {} };\n    },\n});\n",
+      "code": "/**\n * @fileoverview Add a description for your module here.\n */\nimport { createConfigurator } from \"./a2/connector-manager\";\nexport { invoke as default, describe };\nconst { invoke, describe } = createConfigurator({\n    title: \"GMail\",\n    initialize: async () => {\n        return { title: \"GMail\", configuration: {} };\n    },\n});\n",
       "metadata": {
         "title": "configurator",
         "source": {
@@ -61,7 +66,7 @@
       }
     },
     "get-emails": {
-      "code": "/**\n * @fileoverview Gets a list of emails\n */\nimport {} from \"@describe\";\nimport { err } from \"./a2/utils\";\nimport fetch from \"@fetch\";\nimport secrets from \"@secrets\";\nfunction maybeExtractError(e) {\n    try {\n        const parsed = JSON.parse(e);\n        return parsed.error.message;\n    }\n    catch (error) {\n        return e;\n    }\n}\nimport { ok } from \"./a2/utils\";\nexport { invoke as default, describe };\nasync function invoke() {\n    // Get an authentication token.\n    const key = \"connection:$sign-in\";\n    const token = (await secrets({ keys: [key] }))[key];\n    // Call the API.\n    const url = \"https://staging-appcatalyst.sandbox.googleapis.com/v1beta1/executeStep\";\n    const fetchResult = await fetch({\n        url: url,\n        method: \"POST\",\n        headers: {\n            \"Content-Type\": \"application/json\",\n            Authorization: `Bearer ${token}`,\n        },\n        body: {\n            planStep: {\n                stepName: \"get_emails\",\n                modelApi: \"get_emails\",\n                output: \"emails\",\n                inputParameters: [],\n                isListOutput: false,\n            },\n            app_integration_token: token,\n        },\n    });\n    let $error = \"Unknown error\";\n    if (!ok(fetchResult)) {\n        const { status, $error: errObject } = fetchResult;\n        console.warn($error);\n        if (!status) {\n            // This is not an error response, presume fatal error.\n            return { $error };\n        }\n        $error = maybeExtractError(errObject);\n        return { $error };\n    }\n    const response = fetchResult.response;\n    const data = response.executionOutputs[\"get_emails\"].chunks.at(0)?.data;\n    if (!data) {\n        return err(`Invalid response`);\n    }\n    const emails = atob(data);\n    return { emails };\n}\nasync function describe() {\n    return {\n        title: \"Get emails\",\n        description: \"Gets a list of latest 10 unread emails\",\n        metadata: {\n            icon: \"email\",\n            tags: [\"tool\"],\n        },\n        inputSchema: {\n            type: \"object\",\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                emails: {\n                    type: \"string\",\n                    title: \"Unread emails\",\n                },\n            },\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Gets a list of emails\n */\nimport { err } from \"./a2/utils\";\nimport fetch from \"@fetch\";\nimport secrets from \"@secrets\";\nfunction maybeExtractError(e) {\n    try {\n        const parsed = JSON.parse(e);\n        return parsed.error.message;\n    }\n    catch (error) {\n        return e;\n    }\n}\nimport { ok } from \"./a2/utils\";\nexport { invoke as default, describe };\nasync function invoke() {\n    // Get an authentication token.\n    const key = \"connection:$sign-in\";\n    const token = (await secrets({ keys: [key] }))[key];\n    // Call the API.\n    const url = \"https://staging-appcatalyst.sandbox.googleapis.com/v1beta1/executeStep\";\n    const fetchResult = await fetch({\n        url: url,\n        method: \"POST\",\n        headers: {\n            \"Content-Type\": \"application/json\",\n            Authorization: `Bearer ${token}`,\n        },\n        body: {\n            planStep: {\n                stepName: \"get_emails\",\n                modelApi: \"get_emails\",\n                output: \"emails\",\n                inputParameters: [],\n                isListOutput: false,\n            },\n            app_integration_token: token,\n        },\n    });\n    let $error = \"Unknown error\";\n    if (!ok(fetchResult)) {\n        const { status, $error: errObject } = fetchResult;\n        console.warn($error);\n        if (!status) {\n            // This is not an error response, presume fatal error.\n            return { $error };\n        }\n        $error = maybeExtractError(errObject);\n        return { $error };\n    }\n    const response = fetchResult.response;\n    const data = response.executionOutputs[\"get_emails\"].chunks.at(0)?.data;\n    if (!data) {\n        return err(`Invalid response`);\n    }\n    const emails = atob(data);\n    return { emails };\n}\nasync function describe() {\n    return {\n        title: \"Get emails\",\n        description: \"Gets a list of latest 10 unread emails\",\n        metadata: {\n            icon: \"email\",\n            tags: [\"tool\"],\n        },\n        inputSchema: {\n            type: \"object\",\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                emails: {\n                    type: \"string\",\n                    title: \"Unread emails\",\n                },\n            },\n        },\n    };\n}\n",
       "metadata": {
         "title": "get-emails",
         "source": {
@@ -78,5 +83,8 @@
       "url": "./a2.bgl.json"
     }
   },
-  "exports": ["#module:configurator", "#module:get-emails"]
-}
+  "exports": [
+    "#module:configurator",
+    "#module:get-emails"
+  ]
+}
\ No newline at end of file
diff --git a/packages/a2/bgl/go-over-list.bgl.json b/packages/a2/bgl/go-over-list.bgl.json
index bab83fa5927..0f0ea7f602e 100644
--- a/packages/a2/bgl/go-over-list.bgl.json
+++ b/packages/a2/bgl/go-over-list.bgl.json
@@ -51,7 +51,7 @@
   },
   "modules": {
     "main": {
-      "code": "/**\n * @fileoverview Break an objective into tasks and then execute them.\n */\nimport { Template } from \"./a2/template\";\nimport { ToolManager } from \"./a2/tool-manager\";\nimport { GeminiPrompt } from \"./a2/gemini-prompt\";\nimport {} from \"./a2/gemini\";\nimport {} from \"./a2/common\";\nimport { ok, err, toLLMContent, llm, generateId } from \"./a2/utils\";\nimport { report } from \"./a2/output\";\nimport { StructuredResponse } from \"./a2/structured-response\";\nimport { ArgumentNameGenerator } from \"./a2/introducer\";\nimport { plannerPrompt, getPlan } from \"./planner-prompt\";\nimport { Runtime } from \"./runtime\";\nimport { ParallelStrategist } from \"./parallel-strategist\";\nimport { SequentialStrategist } from \"./sequential-strategist\";\nimport { ThinkStrategist } from \"./think-strategist\";\nimport { ConversationalThinkStrategist } from \"./conversational-think-strategist\";\nimport {} from \"./types\";\nimport { fanOutContext } from \"./a2/lists\";\nimport { readSettings } from \"./a2/settings\";\nexport { invoke as default, describe };\nconst STRATEGISTS = [\n    new ParallelStrategist(),\n    new SequentialStrategist(),\n    new ThinkStrategist(),\n    new ConversationalThinkStrategist(),\n];\nfunction findStrategist(name) {\n    if (!name)\n        return STRATEGISTS[0];\n    return STRATEGISTS.find((strategist) => strategist.name === name);\n}\nasync function invoke({ context, plan: objective, strategy, \"z-list\": makeList, ...params }) {\n    const toolManager = new ToolManager(new ArgumentNameGenerator());\n    const template = new Template(objective);\n    const substituting = await template.substitute(params, async ({ path: url, instance }) => toolManager.addTool(url, instance));\n    if (!ok(substituting))\n        return substituting;\n    const strategist = findStrategist(strategy);\n    if (!strategist) {\n        return err(`Unknown strategy: \"${strategy}\"`);\n    }\n    const result = await fanOutContext(substituting, context, async (objective, context, isList) => {\n        const disallowNestedLists = makeList && !isList;\n        const executor = new Runtime(context, toolManager, disallowNestedLists);\n        const executingOne = await executor.executeStrategy(objective, strategist);\n        if (!ok(executingOne))\n            return executingOne;\n        // Disallow making a list when already inside of a make list\n        if (disallowNestedLists) {\n            return {\n                role: \"model\",\n                parts: [\n                    {\n                        id: generateId(),\n                        list: executingOne.map((item) => {\n                            return { content: [item] };\n                        }),\n                    },\n                ],\n            };\n        }\n        const oneContent = {\n            role: \"model\",\n            parts: executingOne.flatMap((item) => {\n                return item.parts;\n            }),\n        };\n        return oneContent;\n    });\n    if (!ok(result))\n        return result;\n    return { context: result };\n}\nasync function describe({ inputs: { plan } }) {\n    const template = new Template(plan);\n    const settings = await readSettings();\n    const experimental = ok(settings) && !!settings[\"Show Experimental Components\"];\n    let extra = {};\n    if (experimental) {\n        extra = {\n            \"z-list\": {\n                type: \"boolean\",\n                title: \"Make a list\",\n                behavior: [\"config\", \"hint-preview\"],\n                icon: \"summarize\",\n                description: \"When checked, this step will try to create a list as its output. Make sure that the prompt asks for a list of some sort\",\n            },\n        };\n    }\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                plan: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-preview\"],\n                    title: \"Objective\",\n                    description: \"Describe what will be turned into a list and then gone over\",\n                },\n                strategy: {\n                    title: \"Strategy\",\n                    description: `How to go over the list: \n\"${STRATEGISTS[0].name}\" is fastest, working in parallel. \n\"${STRATEGISTS[1].name}\" will build on previous work.\n\"${STRATEGISTS[2].name}\" will think after each step adjust the list if necessary`,\n                    type: \"string\",\n                    behavior: [\"config\", \"hint-preview\"],\n                    enum: STRATEGISTS.map((strategist) => strategist.name),\n                    icon: \"joiner\",\n                    default: STRATEGISTS[0].name,\n                },\n                ...extra,\n                ...template.schemas(),\n            },\n            behavior: [\"at-wireable\"],\n            ...template.requireds(),\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Results\",\n                    behavior: [\"main-port\"],\n                },\n            },\n            additionalProperties: false,\n        },\n        title: \"Plan and Execute\",\n        description: \"Break an objective into tasks and then execute them\",\n        metadata: {\n            icon: \"laps\",\n            tags: [\"quick-access\", \"generative\", \"experimental\"],\n            order: 102,\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Break an objective into tasks and then execute them.\n */\nimport { Template } from \"./a2/template\";\nimport { ToolManager } from \"./a2/tool-manager\";\nimport { ok, err, generateId } from \"./a2/utils\";\nimport { ArgumentNameGenerator } from \"./a2/introducer\";\nimport { Runtime } from \"./runtime\";\nimport { ParallelStrategist } from \"./parallel-strategist\";\nimport { SequentialStrategist } from \"./sequential-strategist\";\nimport { ThinkStrategist } from \"./think-strategist\";\nimport { ConversationalThinkStrategist } from \"./conversational-think-strategist\";\nimport { fanOutContext } from \"./a2/lists\";\nimport { readSettings } from \"./a2/settings\";\nexport { invoke as default, describe };\nconst STRATEGISTS = [\n    new ParallelStrategist(),\n    new SequentialStrategist(),\n    new ThinkStrategist(),\n    new ConversationalThinkStrategist(),\n];\nfunction findStrategist(name) {\n    if (!name)\n        return STRATEGISTS[0];\n    return STRATEGISTS.find((strategist) => strategist.name === name);\n}\nasync function invoke({ context, plan: objective, strategy, \"z-list\": makeList, ...params }) {\n    const toolManager = new ToolManager(new ArgumentNameGenerator());\n    const template = new Template(objective);\n    const substituting = await template.substitute(params, async ({ path: url, instance }) => toolManager.addTool(url, instance));\n    if (!ok(substituting))\n        return substituting;\n    const strategist = findStrategist(strategy);\n    if (!strategist) {\n        return err(`Unknown strategy: \"${strategy}\"`);\n    }\n    const result = await fanOutContext(substituting, context, async (objective, context, isList) => {\n        const disallowNestedLists = makeList && !isList;\n        const executor = new Runtime(context, toolManager, disallowNestedLists);\n        const executingOne = await executor.executeStrategy(objective, strategist);\n        if (!ok(executingOne))\n            return executingOne;\n        // Disallow making a list when already inside of a make list\n        if (disallowNestedLists) {\n            return {\n                role: \"model\",\n                parts: [\n                    {\n                        id: generateId(),\n                        list: executingOne.map((item) => {\n                            return { content: [item] };\n                        }),\n                    },\n                ],\n            };\n        }\n        const oneContent = {\n            role: \"model\",\n            parts: executingOne.flatMap((item) => {\n                return item.parts;\n            }),\n        };\n        return oneContent;\n    });\n    if (!ok(result))\n        return result;\n    return { context: result };\n}\nasync function describe({ inputs: { plan } }) {\n    const template = new Template(plan);\n    const settings = await readSettings();\n    const experimental = ok(settings) && !!settings[\"Show Experimental Components\"];\n    let extra = {};\n    if (experimental) {\n        extra = {\n            \"z-list\": {\n                type: \"boolean\",\n                title: \"Make a list\",\n                behavior: [\"config\", \"hint-preview\"],\n                icon: \"summarize\",\n                description: \"When checked, this step will try to create a list as its output. Make sure that the prompt asks for a list of some sort\",\n            },\n        };\n    }\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                plan: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-preview\"],\n                    title: \"Objective\",\n                    description: \"Describe what will be turned into a list and then gone over\",\n                },\n                strategy: {\n                    title: \"Strategy\",\n                    description: `How to go over the list: \n\"${STRATEGISTS[0].name}\" is fastest, working in parallel. \n\"${STRATEGISTS[1].name}\" will build on previous work.\n\"${STRATEGISTS[2].name}\" will think after each step adjust the list if necessary`,\n                    type: \"string\",\n                    behavior: [\"config\", \"hint-preview\"],\n                    enum: STRATEGISTS.map((strategist) => strategist.name),\n                    icon: \"joiner\",\n                    default: STRATEGISTS[0].name,\n                },\n                ...extra,\n                ...template.schemas(),\n            },\n            behavior: [\"at-wireable\"],\n            ...template.requireds(),\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Results\",\n                    behavior: [\"main-port\"],\n                },\n            },\n            additionalProperties: false,\n        },\n        title: \"Plan and Execute\",\n        description: \"Break an objective into tasks and then execute them\",\n        metadata: {\n            icon: \"laps\",\n            tags: [\"quick-access\", \"generative\", \"experimental\"],\n            order: 102,\n        },\n    };\n}\n",
       "metadata": {
         "title": "main",
         "source": {
@@ -63,7 +63,7 @@
       }
     },
     "planner-prompt": {
-      "code": "/**\n * @fileoverview Contains the planner prompt.\n */\nimport { defaultSafetySettings } from \"./a2/gemini\";\nimport { GeminiPrompt } from \"./a2/gemini-prompt\";\nimport { llm, err } from \"./a2/utils\";\nimport {} from \"./types\";\nexport { plannerPrompt, thinkingPlannerPrompt, getPlan };\nfunction preamble(extraPlannerPrompt) {\n    return `You are a planner. \nYou are to create a precise plan -- a list of tasks -- for a given objective. This plan will be executed by others.\n\n${extraPlannerPrompt}\n\nYour responsibility is to produce a plan that fulfills the objective.\n\nExamine the objective, slow down, take a deep breath.\nNow think: how might you break the objective into tasks that, when all completed, will fulfill the objective?\nNow write out the tasks. Do not add any superflous tasks.\n\nFor each task, also think of a brief label that describes that task and could be used in a UI as a hint to the user.\n\nIf multiple tools are mentioned in the objective, make sure to mention them all in the task so that whoever\nexecutes the task can decide which tool to use.\n`;\n}\nfunction planSchema(organize) {\n    const organizeFlag = (organize\n        ? {\n            summarizeResults: {\n                type: \"boolean\",\n                description: \"Set to true if and only if the objective calls for summarizing results at the end. Set to false otherwise.\",\n            },\n        }\n        : {});\n    const required = [\"thinking\", \"todo\"];\n    if (organize) {\n        required.push(...Object.keys(organizeFlag));\n    }\n    return {\n        type: \"object\",\n        properties: {\n            thinking: {\n                type: \"string\",\n                description: \"Brief reasoning on why these steps are the right steps to fulfill the objective.\",\n            },\n            todo: {\n                type: \"array\",\n                items: {\n                    type: \"object\",\n                    properties: {\n                        task: {\n                            type: \"string\",\n                            description: \"The task description. Use action-oriented language, starting with a verb that fits the task.\",\n                        },\n                        label: {\n                            description: \"Short, precise label for that describes the task.\",\n                            type: \"string\",\n                        },\n                    },\n                    required: [\"task\", \"label\"],\n                },\n            },\n            ...organizeFlag,\n        },\n        required,\n    };\n}\nfunction getPlan(content) {\n    const planPart = content.parts.at(0);\n    if (!planPart || !(\"json\" in planPart)) {\n        // TODO: Error recovery.\n        return err(`Gemini generated invalid plan`);\n    }\n    console.log(\"PLAN\", planPart.json);\n    return planPart.json;\n}\nfunction prependInstruction(text, plan) {\n    return {\n        ...plan,\n        parts: [...plan.parts, { text }],\n    };\n}\nfunction thinkingPlannerPrompt(context, objective, plan, steps, extraPlannerPrompt) {\n    const instruction = llm `\n${preamble(extraPlannerPrompt)}\n\nYour objective is:\n\n\\`\\`\\`\n\n${objective}\n\n\\`\\`\\`\n\nYour original plan was:\n\n\\`\\`\\`json\n${JSON.stringify(plan)}\n\\`\\`\\`\n\nSo far, you've completed these steps:\n\n${steps.map((step) => `- ${step}`).join(\"\\n\")}\n\nUpdate the plan to ensure that the steps that follow achieve the objective.\nPay particular attention to steps that did not complete successfully and strategize\ndifferent approaches and steps that might be necesssary to complete them.\n\nOnly add steps to the plan that still need to be completed.\nDo not return completed steps as part of the updated plan.\nIf no more steps are needed, return no steps.\n`.asContent();\n    const contents = [...context, instruction];\n    return new GeminiPrompt({\n        body: {\n            contents,\n            safetySettings: defaultSafetySettings(),\n            generationConfig: {\n                responseSchema: planSchema(true),\n                responseMimeType: \"application/json\",\n            },\n        },\n    });\n}\nfunction plannerPrompt(context, objective, extraPlannerPrompt, organize) {\n    context ??= [];\n    const instruction = `${preamble(extraPlannerPrompt)}`;\n    const contents = [...context, prependInstruction(instruction, objective)];\n    return new GeminiPrompt({\n        body: {\n            contents,\n            safetySettings: defaultSafetySettings(),\n            generationConfig: {\n                responseSchema: planSchema(organize),\n                responseMimeType: \"application/json\",\n            },\n        },\n    });\n}\n",
+      "code": "/**\n * @fileoverview Contains the planner prompt.\n */\nimport { defaultSafetySettings } from \"./a2/gemini\";\nimport { GeminiPrompt } from \"./a2/gemini-prompt\";\nimport { llm, err } from \"./a2/utils\";\nexport { plannerPrompt, thinkingPlannerPrompt, getPlan };\nfunction preamble(extraPlannerPrompt) {\n    return `You are a planner. \nYou are to create a precise plan -- a list of tasks -- for a given objective. This plan will be executed by others.\n\n${extraPlannerPrompt}\n\nYour responsibility is to produce a plan that fulfills the objective.\n\nExamine the objective, slow down, take a deep breath.\nNow think: how might you break the objective into tasks that, when all completed, will fulfill the objective?\nNow write out the tasks. Do not add any superflous tasks.\n\nFor each task, also think of a brief label that describes that task and could be used in a UI as a hint to the user.\n\nIf multiple tools are mentioned in the objective, make sure to mention them all in the task so that whoever\nexecutes the task can decide which tool to use.\n`;\n}\nfunction planSchema(organize) {\n    const organizeFlag = (organize\n        ? {\n            summarizeResults: {\n                type: \"boolean\",\n                description: \"Set to true if and only if the objective calls for summarizing results at the end. Set to false otherwise.\",\n            },\n        }\n        : {});\n    const required = [\"thinking\", \"todo\"];\n    if (organize) {\n        required.push(...Object.keys(organizeFlag));\n    }\n    return {\n        type: \"object\",\n        properties: {\n            thinking: {\n                type: \"string\",\n                description: \"Brief reasoning on why these steps are the right steps to fulfill the objective.\",\n            },\n            todo: {\n                type: \"array\",\n                items: {\n                    type: \"object\",\n                    properties: {\n                        task: {\n                            type: \"string\",\n                            description: \"The task description. Use action-oriented language, starting with a verb that fits the task.\",\n                        },\n                        label: {\n                            description: \"Short, precise label for that describes the task.\",\n                            type: \"string\",\n                        },\n                    },\n                    required: [\"task\", \"label\"],\n                },\n            },\n            ...organizeFlag,\n        },\n        required,\n    };\n}\nfunction getPlan(content) {\n    const planPart = content.parts.at(0);\n    if (!planPart || !(\"json\" in planPart)) {\n        // TODO: Error recovery.\n        return err(`Gemini generated invalid plan`);\n    }\n    console.log(\"PLAN\", planPart.json);\n    return planPart.json;\n}\nfunction prependInstruction(text, plan) {\n    return {\n        ...plan,\n        parts: [...plan.parts, { text }],\n    };\n}\nfunction thinkingPlannerPrompt(context, objective, plan, steps, extraPlannerPrompt) {\n    const instruction = llm `\n${preamble(extraPlannerPrompt)}\n\nYour objective is:\n\n\\`\\`\\`\n\n${objective}\n\n\\`\\`\\`\n\nYour original plan was:\n\n\\`\\`\\`json\n${JSON.stringify(plan)}\n\\`\\`\\`\n\nSo far, you've completed these steps:\n\n${steps.map((step) => `- ${step}`).join(\"\\n\")}\n\nUpdate the plan to ensure that the steps that follow achieve the objective.\nPay particular attention to steps that did not complete successfully and strategize\ndifferent approaches and steps that might be necesssary to complete them.\n\nOnly add steps to the plan that still need to be completed.\nDo not return completed steps as part of the updated plan.\nIf no more steps are needed, return no steps.\n`.asContent();\n    const contents = [...context, instruction];\n    return new GeminiPrompt({\n        body: {\n            contents,\n            safetySettings: defaultSafetySettings(),\n            generationConfig: {\n                responseSchema: planSchema(true),\n                responseMimeType: \"application/json\",\n            },\n        },\n    });\n}\nfunction plannerPrompt(context, objective, extraPlannerPrompt, organize) {\n    context ?? (context = []);\n    const instruction = `${preamble(extraPlannerPrompt)}`;\n    const contents = [...context, prependInstruction(instruction, objective)];\n    return new GeminiPrompt({\n        body: {\n            contents,\n            safetySettings: defaultSafetySettings(),\n            generationConfig: {\n                responseSchema: planSchema(organize),\n                responseMimeType: \"application/json\",\n            },\n        },\n    });\n}\n",
       "metadata": {
         "title": "planner-prompt",
         "source": {
@@ -75,7 +75,7 @@
       }
     },
     "conversational-planner-prompt": {
-      "code": "/**\n * @fileoverview Contains the planner prompt for the conversational think as I go.\n */\nimport { defaultSafetySettings } from \"./a2/gemini\";\nimport { GeminiPrompt } from \"./a2/gemini-prompt\";\nimport { llm, err } from \"./a2/utils\";\nimport {} from \"./types\";\nexport { plannerPrompt, thinkingPlannerPrompt, getPlan };\nfunction preamble(userObjective) {\n    return llm `You are an adaptive AI agent controller.\n\nOverall Objective: Your primary goal is to guide an agent to achieve the following objective:\n<AGENT_INSTRUCTIONS>\n${userObjective}\n</AGENT_INSTRUCTIONS>\n`.asContent();\n}\nfunction planSchema(organize) {\n    const organizeFlag = (organize\n        ? {\n            summarizeResults: {\n                type: \"boolean\",\n                description: \"Set to true if and only if the objective calls for summarizing results at the end. Set to false otherwise.\",\n            },\n        }\n        : {});\n    const required = [\"thinking\", \"todo\"];\n    if (organize) {\n        required.push(...Object.keys(organizeFlag));\n    }\n    return {\n        type: \"object\",\n        properties: {\n            thinking: {\n                type: \"string\",\n                description: \"Brief reasoning on why these steps are the right steps to fulfill the objective, or why termination is necessary.\",\n            },\n            todo: {\n                type: \"array\",\n                description: \"The list of tasks to perform. If terminating, this is empty.\", // Updated description\n                items: {\n                    type: \"object\",\n                    properties: {\n                        task: {\n                            type: \"string\",\n                            description: \"The task description. Use action-oriented language, starting with a verb that fits the task.\",\n                        },\n                        label: {\n                            description: \"Short, precise label for that describes the task.\",\n                            type: \"string\",\n                        },\n                    },\n                    required: [\"task\", \"label\"],\n                },\n            },\n            ...organizeFlag,\n        },\n        required,\n    };\n}\nfunction getPlan(content) {\n    const planPart = content.parts.at(0);\n    if (!planPart || !(\"json\" in planPart)) {\n        // TODO: Error recovery.\n        return err(`Gemini generated invalid plan`);\n    }\n    console.log(\"PLAN\", planPart.json);\n    return planPart.json;\n}\nfunction prependInstruction(text, plan) {\n    return {\n        ...plan,\n        parts: [...plan.parts, { text }],\n    };\n}\nfunction thinkingPlannerPrompt(context, objective, plan, steps, extraPlannerPrompt) {\n    const instruction = llm `\n${preamble(JSON.stringify(objective))}\n\nInitial Plan Outline (Guideline Only):\nThis was the originally intended sequence of actions. Use it as a reference, but do not follow it rigidly if the situation changes.\n\n${JSON.stringify(plan)}\n\nCurrent Situation:\n\nHistory: (A summary of steps already completed and their outcomes)\n\n\\`\\`\\`json\n${JSON.stringify(context)}\n\\`\\`\\`\n\nYour Task:\n\nBased on the Overall Objective, the Initial Plan Outline, the Available Tools, the History, and crucially, the Last Outcome, determine the single best next step for the agent to take right now. Your absolute priority is to make progress towards the objective *while acknowledging the user's input*.\n\nCritical Instructions:\n\n1.  **Evaluate Last Outcome:** Carefully analyze the \\`Last Outcome\\`.\n\n2.  **Expected Outcome:** If the outcome was expected (e.g., successful tool execution providing needed info, user response directly answering the previous question) and aligns with progressing the Initial Plan Outline, determine the next logical step from that outline or towards the objective.\n\n3.  **Unexpected/User-Driven Outcome:** If the \\`Last Outcome\\` is unexpected, problematic, reveals new user needs, is off-topic, emotionally charged, evasive, indicates a failure, or otherwise deviates from the expected path:\n    * **PRIORITY 1: Address the User Input:**\n        * **Acknowledge & Validate:** *Explicitly acknowledge* the user's statement, question, or expressed feeling. Use empathetic phrasing. Examples: \"I understand you're asking about X...\", \"That's an interesting point about Y...\", \"It sounds like you're concerned about Z...\", \"Thanks for sharing that perspective on W...\" Avoid language that dismisses or ignores their contribution.\n        * **Assess Relevance & Safety:** Quickly determine if the user's input relates to the objective, introduces a new constraint, signals discomfort, raises ethical flags, or indicates a desire to stop.\n        * **Connect (If Possible):** If the user's point can be briefly and naturally linked back to the overall objective, do so. Example: \"...and thinking about Y might actually help us clarify [part of the objective]...\"\n        * **Empathetically Redirect:** Gently guide the conversation back towards the objective or the next logical step needed to achieve it. Frame it collaboratively. Examples: \"...to make sure we achieve [objective], perhaps we could first look at...?\", \"...I want to make sure I help you with [objective], so could we focus back on...?\", \"Given our goal of [objective], the next step I think would be helpful is...\"\n        * **Adapt the Plan:** Based on the user's input, *fundamentally reassess* the next step. Do NOT blindly follow the Initial Plan Outline. Your next action *must* account for what the user just said.\n    * **PRIORITY 2: Determine Adaptive Next Step:** Based on the above interaction, decide the *actual* next step. This might be:\n        * Asking the user a clarifying question that incorporates their last point.\n        * Rephrasing your previous question or request.\n        * Using a different tool or strategy better suited to the new situation.\n        * Acknowledging an error and proposing a correction.\n        * Pausing the plan to address a user concern directly.\n        * Deciding *not* to proceed if the user signals discomfort or the topic becomes inappropriate.\n        * Ending the interaction politely if the objective is blocked, achieved, or the user wishes to stop.\n\n4.  **Justify (Mandatory for Deviations):** Briefly explain *why* you chose this next step, *especially* if deviating from the original plan due to an unexpected outcome or user input. Explicitly mention how your chosen step acknowledges the user's input and aims to get back on track empathetically.\n\n5.  **Specify Action:** Clearly define the single next action for the agent.\n    * **User Interaction:** Provide the *exact text* the agent should say to the user. Otherwise we will confuse the user.\n    * **Tool Use:** State the tool name and the precise inputs required, ensuring inputs are updated based on the latest context and user feedback.\n    * **Internal Step:** Describe the internal calculation, data processing, or analysis the agent needs to perform.\n\n6. Note when the agent finishes executing, we'll automatically show the results to the user, so there's no need for an explicit 'present' or 'show' step.\n\nOutput:\nProvide a clear description of the next step the agent should execute now, and a list of good steps to follow after that (generate them assuming the single next step was sucessful).\n\nFor example,\n\\`\\`\\`json\n{\n    \"summarizeResults\": false,\n    \"thinking\": \"The user responded with \\\"hot dog please\\\" after the introduction. This is nonsensical and off-topic. I should acknowledge their input and try to redirect the conversation back to the original goal.\",\n    \"todo\": [\n        {\n            \"label\": \"Gather Information\",\n            \"task\": \"Tell the user that while a hotdog sure sounds tasty, I can't offer the user a hotdog. Then ask the user what brand or business they would like to highlight in the post.\"\n        },\n        {\n            \"label\": \"Generate Post\",\n            \"task\": \"Use the brand name and description to generate an instagram post.\"\n        }\n    ]\n}\n\\`\\`\\`\n\nExtra instructions:\n${extraPlannerPrompt}\n`.asContent();\n    const contents = [instruction];\n    let prompt = new GeminiPrompt({\n        body: {\n            contents,\n            safetySettings: defaultSafetySettings(),\n            generationConfig: {\n                responseSchema: planSchema(true),\n                responseMimeType: \"application/json\",\n            },\n        },\n    });\n    console.log(\"thinkingPlannerPrompt: \", prompt);\n    return prompt;\n}\nfunction plannerPrompt(context, objective, extraPlannerPrompt, organize) {\n    context ??= [];\n    const instruction = preamble(JSON.stringify(objective));\n    const epilogue = llm `\nExtra instructions:\n${extraPlannerPrompt}\n`.asContent();\n    const contents = [...context, instruction, epilogue];\n    let prompt = new GeminiPrompt({\n        body: {\n            contents,\n            safetySettings: defaultSafetySettings(),\n            generationConfig: {\n                responseSchema: planSchema(organize),\n                responseMimeType: \"application/json\",\n            },\n        },\n    });\n    console.log(\"plannerPrompt: \", prompt);\n    return prompt;\n}\n",
+      "code": "/**\n * @fileoverview Contains the planner prompt for the conversational think as I go.\n */\nimport { defaultSafetySettings } from \"./a2/gemini\";\nimport { GeminiPrompt } from \"./a2/gemini-prompt\";\nimport { llm, err } from \"./a2/utils\";\nexport { plannerPrompt, thinkingPlannerPrompt, getPlan };\nfunction preamble(userObjective) {\n    return llm `You are an adaptive AI agent controller.\n\nOverall Objective: Your primary goal is to guide an agent to achieve the following objective:\n<AGENT_INSTRUCTIONS>\n${userObjective}\n</AGENT_INSTRUCTIONS>\n`.asContent();\n}\nfunction planSchema(organize) {\n    const organizeFlag = (organize\n        ? {\n            summarizeResults: {\n                type: \"boolean\",\n                description: \"Set to true if and only if the objective calls for summarizing results at the end. Set to false otherwise.\",\n            },\n        }\n        : {});\n    const required = [\"thinking\", \"todo\"];\n    if (organize) {\n        required.push(...Object.keys(organizeFlag));\n    }\n    return {\n        type: \"object\",\n        properties: {\n            thinking: {\n                type: \"string\",\n                description: \"Brief reasoning on why these steps are the right steps to fulfill the objective, or why termination is necessary.\",\n            },\n            todo: {\n                type: \"array\",\n                description: \"The list of tasks to perform. If terminating, this is empty.\", // Updated description\n                items: {\n                    type: \"object\",\n                    properties: {\n                        task: {\n                            type: \"string\",\n                            description: \"The task description. Use action-oriented language, starting with a verb that fits the task.\",\n                        },\n                        label: {\n                            description: \"Short, precise label for that describes the task.\",\n                            type: \"string\",\n                        },\n                    },\n                    required: [\"task\", \"label\"],\n                },\n            },\n            ...organizeFlag,\n        },\n        required,\n    };\n}\nfunction getPlan(content) {\n    const planPart = content.parts.at(0);\n    if (!planPart || !(\"json\" in planPart)) {\n        // TODO: Error recovery.\n        return err(`Gemini generated invalid plan`);\n    }\n    console.log(\"PLAN\", planPart.json);\n    return planPart.json;\n}\nfunction prependInstruction(text, plan) {\n    return {\n        ...plan,\n        parts: [...plan.parts, { text }],\n    };\n}\nfunction thinkingPlannerPrompt(context, objective, plan, steps, extraPlannerPrompt) {\n    const instruction = llm `\n${preamble(JSON.stringify(objective))}\n\nInitial Plan Outline (Guideline Only):\nThis was the originally intended sequence of actions. Use it as a reference, but do not follow it rigidly if the situation changes.\n\n${JSON.stringify(plan)}\n\nCurrent Situation:\n\nHistory: (A summary of steps already completed and their outcomes)\n\n\\`\\`\\`json\n${JSON.stringify(context)}\n\\`\\`\\`\n\nYour Task:\n\nBased on the Overall Objective, the Initial Plan Outline, the Available Tools, the History, and crucially, the Last Outcome, determine the single best next step for the agent to take right now. Your absolute priority is to make progress towards the objective *while acknowledging the user's input*.\n\nCritical Instructions:\n\n1.  **Evaluate Last Outcome:** Carefully analyze the \\`Last Outcome\\`.\n\n2.  **Expected Outcome:** If the outcome was expected (e.g., successful tool execution providing needed info, user response directly answering the previous question) and aligns with progressing the Initial Plan Outline, determine the next logical step from that outline or towards the objective.\n\n3.  **Unexpected/User-Driven Outcome:** If the \\`Last Outcome\\` is unexpected, problematic, reveals new user needs, is off-topic, emotionally charged, evasive, indicates a failure, or otherwise deviates from the expected path:\n    * **PRIORITY 1: Address the User Input:**\n        * **Acknowledge & Validate:** *Explicitly acknowledge* the user's statement, question, or expressed feeling. Use empathetic phrasing. Examples: \"I understand you're asking about X...\", \"That's an interesting point about Y...\", \"It sounds like you're concerned about Z...\", \"Thanks for sharing that perspective on W...\" Avoid language that dismisses or ignores their contribution.\n        * **Assess Relevance & Safety:** Quickly determine if the user's input relates to the objective, introduces a new constraint, signals discomfort, raises ethical flags, or indicates a desire to stop.\n        * **Connect (If Possible):** If the user's point can be briefly and naturally linked back to the overall objective, do so. Example: \"...and thinking about Y might actually help us clarify [part of the objective]...\"\n        * **Empathetically Redirect:** Gently guide the conversation back towards the objective or the next logical step needed to achieve it. Frame it collaboratively. Examples: \"...to make sure we achieve [objective], perhaps we could first look at...?\", \"...I want to make sure I help you with [objective], so could we focus back on...?\", \"Given our goal of [objective], the next step I think would be helpful is...\"\n        * **Adapt the Plan:** Based on the user's input, *fundamentally reassess* the next step. Do NOT blindly follow the Initial Plan Outline. Your next action *must* account for what the user just said.\n    * **PRIORITY 2: Determine Adaptive Next Step:** Based on the above interaction, decide the *actual* next step. This might be:\n        * Asking the user a clarifying question that incorporates their last point.\n        * Rephrasing your previous question or request.\n        * Using a different tool or strategy better suited to the new situation.\n        * Acknowledging an error and proposing a correction.\n        * Pausing the plan to address a user concern directly.\n        * Deciding *not* to proceed if the user signals discomfort or the topic becomes inappropriate.\n        * Ending the interaction politely if the objective is blocked, achieved, or the user wishes to stop.\n\n4.  **Justify (Mandatory for Deviations):** Briefly explain *why* you chose this next step, *especially* if deviating from the original plan due to an unexpected outcome or user input. Explicitly mention how your chosen step acknowledges the user's input and aims to get back on track empathetically.\n\n5.  **Specify Action:** Clearly define the single next action for the agent.\n    * **User Interaction:** Provide the *exact text* the agent should say to the user. Otherwise we will confuse the user.\n    * **Tool Use:** State the tool name and the precise inputs required, ensuring inputs are updated based on the latest context and user feedback.\n    * **Internal Step:** Describe the internal calculation, data processing, or analysis the agent needs to perform.\n\n6. Note when the agent finishes executing, we'll automatically show the results to the user, so there's no need for an explicit 'present' or 'show' step.\n\nOutput:\nProvide a clear description of the next step the agent should execute now, and a list of good steps to follow after that (generate them assuming the single next step was sucessful).\n\nFor example,\n\\`\\`\\`json\n{\n    \"summarizeResults\": false,\n    \"thinking\": \"The user responded with \\\"hot dog please\\\" after the introduction. This is nonsensical and off-topic. I should acknowledge their input and try to redirect the conversation back to the original goal.\",\n    \"todo\": [\n        {\n            \"label\": \"Gather Information\",\n            \"task\": \"Tell the user that while a hotdog sure sounds tasty, I can't offer the user a hotdog. Then ask the user what brand or business they would like to highlight in the post.\"\n        },\n        {\n            \"label\": \"Generate Post\",\n            \"task\": \"Use the brand name and description to generate an instagram post.\"\n        }\n    ]\n}\n\\`\\`\\`\n\nExtra instructions:\n${extraPlannerPrompt}\n`.asContent();\n    const contents = [instruction];\n    let prompt = new GeminiPrompt({\n        body: {\n            contents,\n            safetySettings: defaultSafetySettings(),\n            generationConfig: {\n                responseSchema: planSchema(true),\n                responseMimeType: \"application/json\",\n            },\n        },\n    });\n    console.log(\"thinkingPlannerPrompt: \", prompt);\n    return prompt;\n}\nfunction plannerPrompt(context, objective, extraPlannerPrompt, organize) {\n    context ?? (context = []);\n    const instruction = preamble(JSON.stringify(objective));\n    const epilogue = llm `\nExtra instructions:\n${extraPlannerPrompt}\n`.asContent();\n    const contents = [...context, instruction, epilogue];\n    let prompt = new GeminiPrompt({\n        body: {\n            contents,\n            safetySettings: defaultSafetySettings(),\n            generationConfig: {\n                responseSchema: planSchema(organize),\n                responseMimeType: \"application/json\",\n            },\n        },\n    });\n    console.log(\"plannerPrompt: \", prompt);\n    return prompt;\n}\n",
       "metadata": {
         "title": "conversational-planner-prompt",
         "source": {
@@ -87,7 +87,7 @@
       }
     },
     "types": {
-      "code": "/**\n * @fileoverview Common types.\n */\n",
+      "code": "/**\n * @fileoverview Common types.\n */\nexport {};\n",
       "metadata": {
         "title": "types",
         "source": {
@@ -99,7 +99,7 @@
       }
     },
     "runtime": {
-      "code": "/**\n * @fileoverview The runtime that powers going over the list.\n */\nimport { ToolManager } from \"./a2/tool-manager\";\nimport { StructuredResponse } from \"./a2/structured-response\";\nimport { ok, err, toLLMContent } from \"./a2/utils\";\nimport { GeminiPrompt } from \"./a2/gemini-prompt\";\nimport {} from \"./types\";\nexport { Runtime, generateId };\nfunction generateId() {\n    return Math.random().toString(36).substring(2, 5);\n}\nclass Runtime {\n    toolManager;\n    makeList;\n    context;\n    errors = [];\n    execute;\n    constructor(context, toolManager, makeList) {\n        this.toolManager = toolManager;\n        this.makeList = makeList;\n        this.context = context ? [...context] : [];\n        this.execute = this.#execute.bind(this);\n    }\n    async executeStrategy(objective, strategist) {\n        return strategist.execute(this.execute, this.context, objective, this.makeList);\n    }\n    async #execute(item) {\n        const { toolManager, context, errors } = this;\n        let structuredResponse;\n        const prompt = toLLMContent(item.task);\n        let contents;\n        let toolConfig = {};\n        if (!toolManager.hasTools()) {\n            structuredResponse = new StructuredResponse(generateId(), false);\n            contents = structuredResponse.addPrompt(context, prompt);\n        }\n        else {\n            toolConfig = {\n                toolConfig: {\n                    functionCallingConfig: {\n                        mode: \"ANY\",\n                    },\n                },\n            };\n            contents = [...context, toLLMContent(item.task)];\n        }\n        const executing = await new GeminiPrompt({\n            body: {\n                contents,\n                tools: toolManager.list(),\n                ...toolConfig,\n            },\n            systemInstruction: structuredResponse?.instruction(),\n        }, {\n            toolManager,\n            allowToolErrors: true,\n            validator: (content) => {\n                return structuredResponse?.parseContent(content);\n            },\n        }).invoke();\n        if (!ok(executing)) {\n            errors.push(executing.$error);\n            return;\n        }\n        return structuredResponse\n            ? toLLMContent(structuredResponse.body, \"model\")\n            : executing.last;\n    }\n}\n",
+      "code": "/**\n * @fileoverview The runtime that powers going over the list.\n */\nvar __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n    return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n};\nvar _Runtime_instances, _Runtime_execute;\nimport { StructuredResponse } from \"./a2/structured-response\";\nimport { ok, toLLMContent } from \"./a2/utils\";\nimport { GeminiPrompt } from \"./a2/gemini-prompt\";\nexport { Runtime, generateId };\nfunction generateId() {\n    return Math.random().toString(36).substring(2, 5);\n}\nclass Runtime {\n    constructor(context, toolManager, makeList) {\n        _Runtime_instances.add(this);\n        this.toolManager = toolManager;\n        this.makeList = makeList;\n        this.errors = [];\n        this.context = context ? [...context] : [];\n        this.execute = __classPrivateFieldGet(this, _Runtime_instances, \"m\", _Runtime_execute).bind(this);\n    }\n    async executeStrategy(objective, strategist) {\n        return strategist.execute(this.execute, this.context, objective, this.makeList);\n    }\n}\n_Runtime_instances = new WeakSet(), _Runtime_execute = async function _Runtime_execute(item) {\n    const { toolManager, context, errors } = this;\n    let structuredResponse;\n    const prompt = toLLMContent(item.task);\n    let contents;\n    let toolConfig = {};\n    if (!toolManager.hasTools()) {\n        structuredResponse = new StructuredResponse(generateId(), false);\n        contents = structuredResponse.addPrompt(context, prompt);\n    }\n    else {\n        toolConfig = {\n            toolConfig: {\n                functionCallingConfig: {\n                    mode: \"ANY\",\n                },\n            },\n        };\n        contents = [...context, toLLMContent(item.task)];\n    }\n    const executing = await new GeminiPrompt({\n        body: {\n            contents,\n            tools: toolManager.list(),\n            ...toolConfig,\n        },\n        systemInstruction: structuredResponse?.instruction(),\n    }, {\n        toolManager,\n        allowToolErrors: true,\n        validator: (content) => {\n            return structuredResponse?.parseContent(content);\n        },\n    }).invoke();\n    if (!ok(executing)) {\n        errors.push(executing.$error);\n        return;\n    }\n    return structuredResponse\n        ? toLLMContent(structuredResponse.body, \"model\")\n        : executing.last;\n};\n",
       "metadata": {
         "title": "runtime",
         "source": {
@@ -111,7 +111,7 @@
       }
     },
     "parallel-strategist": {
-      "code": "/**\n * @fileoverview Executes a parallel strategy.\n */\nimport { report } from \"./a2/output\";\nimport { ok } from \"./a2/utils\";\nimport { plannerPrompt, getPlan } from \"./planner-prompt\";\nimport {} from \"./types\";\nexport { ParallelStrategist };\nclass ParallelStrategist {\n    name = \"All at once\";\n    extraPlannerPrompt = `\nAll tasks in the plan will be executed in any order or all at once, so make sure that the tasks don't depend on each other.\nThink carefully: for every task in the list, does any task depend on another task? If so, rethink your list\nuntil all tasks are indepedent`;\n    async execute(execute, mutableContext, objective) {\n        const planning = await plannerPrompt(mutableContext, objective, this.extraPlannerPrompt, false).invoke();\n        if (!ok(planning))\n            return planning;\n        const plan = getPlan(planning.last);\n        if (!ok(plan))\n            return plan;\n        await report({\n            actor: \"Planner\",\n            category: `Creating a plan`,\n            name: \"Here's my list\",\n            icon: \"laps\",\n            details: `\n${plan.todo.map((item) => `- ${item.label}`).join(\"\\n\")}\n\nI will now work on all items at the same time.`,\n        });\n        return (await Promise.all(plan.todo.map(execute))).filter((item) => !!item);\n    }\n}\n",
+      "code": "/**\n * @fileoverview Executes a parallel strategy.\n */\nimport { report } from \"./a2/output\";\nimport { ok } from \"./a2/utils\";\nimport { plannerPrompt, getPlan } from \"./planner-prompt\";\nexport { ParallelStrategist };\nclass ParallelStrategist {\n    constructor() {\n        this.name = \"All at once\";\n        this.extraPlannerPrompt = `\nAll tasks in the plan will be executed in any order or all at once, so make sure that the tasks don't depend on each other.\nThink carefully: for every task in the list, does any task depend on another task? If so, rethink your list\nuntil all tasks are indepedent`;\n    }\n    async execute(execute, mutableContext, objective) {\n        const planning = await plannerPrompt(mutableContext, objective, this.extraPlannerPrompt, false).invoke();\n        if (!ok(planning))\n            return planning;\n        const plan = getPlan(planning.last);\n        if (!ok(plan))\n            return plan;\n        await report({\n            actor: \"Planner\",\n            category: `Creating a plan`,\n            name: \"Here's my list\",\n            icon: \"laps\",\n            details: `\n${plan.todo.map((item) => `- ${item.label}`).join(\"\\n\")}\n\nI will now work on all items at the same time.`,\n        });\n        return (await Promise.all(plan.todo.map(execute))).filter((item) => !!item);\n    }\n}\n",
       "metadata": {
         "title": "parallel-strategist",
         "source": {
@@ -123,7 +123,7 @@
       }
     },
     "sequential-strategist": {
-      "code": "/**\n * @fileoverview Executes sequential strategy.\n */\nimport {} from \"./types\";\nimport { plannerPrompt, getPlan } from \"./planner-prompt\";\nimport { toLLMContent, ok } from \"./a2/utils\";\nimport { report } from \"./a2/output\";\nexport { SequentialStrategist };\nclass SequentialStrategist {\n    name = \"Go in order\";\n    extraPlannerPrompt = `\nAll tasks in the plan will be executed in sequence, building on each other.`;\n    async execute(execute, mutableContext, objective) {\n        const planning = await plannerPrompt(mutableContext, objective, this.extraPlannerPrompt, false).invoke();\n        if (!ok(planning))\n            return planning;\n        const plan = getPlan(planning.last);\n        if (!ok(plan))\n            return plan;\n        await report({\n            actor: \"Planner\",\n            category: `Creating a list`,\n            name: \"Here's my list\",\n            icon: \"laps\",\n            details: `\n${plan.todo.map((item) => `1. ${item.label}`).join(\"\\n\")}\n\nI will now go over the list in order.`,\n        });\n        const results = [];\n        for (const task of plan.todo) {\n            await report({\n                actor: \"Worker\",\n                category: \"Working on a list item\",\n                name: \"Item\",\n                icon: \"laps\",\n                details: `Currently working on:\n  \n  ${task.task}\n  `,\n            });\n            const result = await execute(task);\n            if (result) {\n                mutableContext.push(toLLMContent(task.task), result);\n                results.push(result);\n            }\n        }\n        return results;\n    }\n}\n",
+      "code": "/**\n * @fileoverview Executes sequential strategy.\n */\nimport { plannerPrompt, getPlan } from \"./planner-prompt\";\nimport { toLLMContent, ok } from \"./a2/utils\";\nimport { report } from \"./a2/output\";\nexport { SequentialStrategist };\nclass SequentialStrategist {\n    constructor() {\n        this.name = \"Go in order\";\n        this.extraPlannerPrompt = `\nAll tasks in the plan will be executed in sequence, building on each other.`;\n    }\n    async execute(execute, mutableContext, objective) {\n        const planning = await plannerPrompt(mutableContext, objective, this.extraPlannerPrompt, false).invoke();\n        if (!ok(planning))\n            return planning;\n        const plan = getPlan(planning.last);\n        if (!ok(plan))\n            return plan;\n        await report({\n            actor: \"Planner\",\n            category: `Creating a list`,\n            name: \"Here's my list\",\n            icon: \"laps\",\n            details: `\n${plan.todo.map((item) => `1. ${item.label}`).join(\"\\n\")}\n\nI will now go over the list in order.`,\n        });\n        const results = [];\n        for (const task of plan.todo) {\n            await report({\n                actor: \"Worker\",\n                category: \"Working on a list item\",\n                name: \"Item\",\n                icon: \"laps\",\n                details: `Currently working on:\n  \n  ${task.task}\n  `,\n            });\n            const result = await execute(task);\n            if (result) {\n                mutableContext.push(toLLMContent(task.task), result);\n                results.push(result);\n            }\n        }\n        return results;\n    }\n}\n",
       "metadata": {
         "title": "sequential-strategist",
         "source": {
@@ -135,7 +135,7 @@
       }
     },
     "think-strategist": {
-      "code": "/**\n * @fileoverview Executes think-as-i strategy.\n */\nimport {} from \"./types\";\nimport { ok, toLLMContent } from \"./a2/utils\";\nimport { report } from \"./a2/output\";\nimport { organizerPrompt } from \"./organizer-prompt\";\nimport { plannerPrompt, thinkingPlannerPrompt, getPlan, } from \"./planner-prompt\";\nexport { ThinkStrategist };\n/**\n * How many extra steps beyond original plan do we allow\n * before concluding we are in the weeds and giving up.\n */\nconst OVERRUN_BUFFER = 10;\nclass ThinkStrategist {\n    name = \"Think as I go\";\n    tasks = [];\n    extraPlannerPrompt = `\nIf the objective calls to organize or summarize results at the end, do not add that as a step.\nInstead, set the \"organizeResults\" property to \"true\". This will let the organizing agent know\nto kick off the organizing task after you're done.\n\nWhen the objective does not explicitly contain the request to organize or summarize results,\nmake sure to set the \"organizeProperty\" to \"false\". Do not invent new work.\n\nNow think real hard: do you need to organize or summarize results?\n`;\n    async execute(execute, mutableContext, objective, makeList) {\n        const planning = await plannerPrompt(mutableContext, objective, this.extraPlannerPrompt, true).invoke();\n        if (!ok(planning))\n            return planning;\n        let plan = getPlan(planning.last);\n        if (!ok(plan))\n            return plan;\n        const results = [];\n        let max = plan.todo.length + OVERRUN_BUFFER;\n        let organizeResults = false;\n        let planDescription = \"Here is my starting plan\";\n        while (--max) {\n            const task = plan.todo.at(0);\n            if (plan.summarizeResults) {\n                organizeResults = true;\n            }\n            if (!task)\n                break;\n            await report({\n                actor: \"Planner\",\n                category: \"Progress update\",\n                name: \"Thinking\",\n                icon: \"laps\",\n                details: `\n\nHere's my thinking:\n\n${plan.thinking}\n        \n${planDescription}:\n\n${plan.todo.map((item) => `1. ${item.label}`).join(\"\\n\")}\n\nI will now go over the plan in order, thinking after each step\nand adjusting the plan if necessary.`,\n            });\n            const result = await execute(task);\n            if (result) {\n                mutableContext.push(toLLMContent(task.task), result);\n                results.push(result);\n            }\n            this.tasks.push(task.task);\n            const thinking = await thinkingPlannerPrompt(mutableContext, objective, plan, this.tasks, this.extraPlannerPrompt).invoke();\n            if (!ok(thinking))\n                return thinking;\n            const newPlan = getPlan(thinking.last);\n            if (!ok(newPlan))\n                return newPlan;\n            plan = newPlan;\n            planDescription = \"Here are the remaining steps in the plan\";\n        }\n        if (organizeResults) {\n            await report({\n                actor: \"Planner\",\n                category: \"Organizing work into a report\",\n                name: \"Organizing work report\",\n                icon: \"laps\",\n                details: `I will now organize all of my work into a report.`,\n            });\n            const organizing = await organizerPrompt(results, objective, makeList).invoke();\n            if (!ok(organizing))\n                return organizing;\n            return [organizing.last];\n        }\n        return results;\n    }\n}\n",
+      "code": "/**\n * @fileoverview Executes think-as-i strategy.\n */\nimport { ok, toLLMContent } from \"./a2/utils\";\nimport { report } from \"./a2/output\";\nimport { organizerPrompt } from \"./organizer-prompt\";\nimport { plannerPrompt, thinkingPlannerPrompt, getPlan, } from \"./planner-prompt\";\nexport { ThinkStrategist };\n/**\n * How many extra steps beyond original plan do we allow\n * before concluding we are in the weeds and giving up.\n */\nconst OVERRUN_BUFFER = 10;\nclass ThinkStrategist {\n    constructor() {\n        this.name = \"Think as I go\";\n        this.tasks = [];\n        this.extraPlannerPrompt = `\nIf the objective calls to organize or summarize results at the end, do not add that as a step.\nInstead, set the \"organizeResults\" property to \"true\". This will let the organizing agent know\nto kick off the organizing task after you're done.\n\nWhen the objective does not explicitly contain the request to organize or summarize results,\nmake sure to set the \"organizeProperty\" to \"false\". Do not invent new work.\n\nNow think real hard: do you need to organize or summarize results?\n`;\n    }\n    async execute(execute, mutableContext, objective, makeList) {\n        const planning = await plannerPrompt(mutableContext, objective, this.extraPlannerPrompt, true).invoke();\n        if (!ok(planning))\n            return planning;\n        let plan = getPlan(planning.last);\n        if (!ok(plan))\n            return plan;\n        const results = [];\n        let max = plan.todo.length + OVERRUN_BUFFER;\n        let organizeResults = false;\n        let planDescription = \"Here is my starting plan\";\n        while (--max) {\n            const task = plan.todo.at(0);\n            if (plan.summarizeResults) {\n                organizeResults = true;\n            }\n            if (!task)\n                break;\n            await report({\n                actor: \"Planner\",\n                category: \"Progress update\",\n                name: \"Thinking\",\n                icon: \"laps\",\n                details: `\n\nHere's my thinking:\n\n${plan.thinking}\n        \n${planDescription}:\n\n${plan.todo.map((item) => `1. ${item.label}`).join(\"\\n\")}\n\nI will now go over the plan in order, thinking after each step\nand adjusting the plan if necessary.`,\n            });\n            const result = await execute(task);\n            if (result) {\n                mutableContext.push(toLLMContent(task.task), result);\n                results.push(result);\n            }\n            this.tasks.push(task.task);\n            const thinking = await thinkingPlannerPrompt(mutableContext, objective, plan, this.tasks, this.extraPlannerPrompt).invoke();\n            if (!ok(thinking))\n                return thinking;\n            const newPlan = getPlan(thinking.last);\n            if (!ok(newPlan))\n                return newPlan;\n            plan = newPlan;\n            planDescription = \"Here are the remaining steps in the plan\";\n        }\n        if (organizeResults) {\n            await report({\n                actor: \"Planner\",\n                category: \"Organizing work into a report\",\n                name: \"Organizing work report\",\n                icon: \"laps\",\n                details: `I will now organize all of my work into a report.`,\n            });\n            const organizing = await organizerPrompt(results, objective, makeList).invoke();\n            if (!ok(organizing))\n                return organizing;\n            return [organizing.last];\n        }\n        return results;\n    }\n}\n",
       "metadata": {
         "title": "think-strategist",
         "source": {
@@ -147,7 +147,7 @@
       }
     },
     "conversational-think-strategist": {
-      "code": "/**\n * @fileoverview Executes think-as-i strategy, but gently redirects off topic user input back on topic.\n */\nimport {} from \"./types\";\nimport { ok, toLLMContent } from \"./a2/utils\";\nimport { report } from \"./a2/output\";\nimport { organizerPrompt } from \"./organizer-prompt\";\nimport { plannerPrompt, thinkingPlannerPrompt, getPlan, } from \"./conversational-planner-prompt\";\nexport { ConversationalThinkStrategist };\n/**\n * How many extra steps beyond original plan do we allow\n * before concluding we are in the weeds and giving up.\n */\nconst OVERRUN_BUFFER = 10;\nclass ConversationalThinkStrategist {\n    name = \"[Alpha] Conversational Think as I go\";\n    tasks = [];\n    extraPlannerPrompt = `\nIf the objective calls to organize or summarize results at the end, do not add that as a step.\nInstead, set the \"organizeResults\" property to \"true\". This will let the organizing agent know\nto kick off the organizing task after you're done.\n\nWhen the objective does not explicitly contain the request to organize or summarize results,\nmake sure to set the \"organizeProperty\" to \"false\". Do not invent new work.\n\nNow think real hard: do you need to organize or summarize results?\n`;\n    async execute(execute, mutableContext, objective, makeList) {\n        const planning = await plannerPrompt(mutableContext, objective, this.extraPlannerPrompt, true).invoke();\n        if (!ok(planning))\n            return planning;\n        let plan = getPlan(planning.last);\n        if (!ok(plan))\n            return plan;\n        const results = [];\n        let max = plan.todo.length + OVERRUN_BUFFER;\n        let organizeResults = false;\n        let planDescription = \"Here is my starting plan\";\n        while (--max) {\n            const task = plan.todo.at(0);\n            if (plan.summarizeResults) {\n                organizeResults = true;\n            }\n            if (!task)\n                break;\n            await report({\n                actor: \"Planner\",\n                category: \"Progress update\",\n                name: \"Thinking\",\n                icon: \"laps\",\n                details: `\n\nHere's my thinking:\n\n${plan.thinking}\n        \n${planDescription}:\n\n${plan.todo.map((item) => `1. ${item.label}`).join(\"\\n\")}\n\nI will now go over the plan in order, thinking after each step\nand adjusting the plan if necessary.`,\n            });\n            const result = await execute(task);\n            if (result) {\n                // Add an 'Action' label so it's easier for the planner to tell what's an agent action vs response.\n                // Unfortunately every item in the context except for function calls has 'role: user'.\n                mutableContext.push(toLLMContent(\"Action: \" + task.task), result);\n                console.log(\"think-strategist\", mutableContext);\n                results.push(result);\n            }\n            this.tasks.push(task.task);\n            const thinking = await thinkingPlannerPrompt(mutableContext, objective, plan, this.tasks, this.extraPlannerPrompt).invoke();\n            if (!ok(thinking))\n                return thinking;\n            const newPlan = getPlan(thinking.last);\n            if (!ok(newPlan))\n                return newPlan;\n            plan = newPlan;\n            planDescription = \"Here are the remaining steps in the plan\";\n        }\n        if (organizeResults) {\n            await report({\n                actor: \"Planner\",\n                category: \"Organizing work into a report\",\n                name: \"Organizing work report\",\n                icon: \"laps\",\n                details: `I will now organize all of my work into a report.`,\n            });\n            const organizing = await organizerPrompt(results, objective, makeList).invoke();\n            if (!ok(organizing))\n                return organizing;\n            return [organizing.last];\n        }\n        return results;\n    }\n}\n",
+      "code": "/**\n * @fileoverview Executes think-as-i strategy, but gently redirects off topic user input back on topic.\n */\nimport { ok, toLLMContent } from \"./a2/utils\";\nimport { report } from \"./a2/output\";\nimport { organizerPrompt } from \"./organizer-prompt\";\nimport { plannerPrompt, thinkingPlannerPrompt, getPlan, } from \"./conversational-planner-prompt\";\nexport { ConversationalThinkStrategist };\n/**\n * How many extra steps beyond original plan do we allow\n * before concluding we are in the weeds and giving up.\n */\nconst OVERRUN_BUFFER = 10;\nclass ConversationalThinkStrategist {\n    constructor() {\n        this.name = \"[Alpha] Conversational Think as I go\";\n        this.tasks = [];\n        this.extraPlannerPrompt = `\nIf the objective calls to organize or summarize results at the end, do not add that as a step.\nInstead, set the \"organizeResults\" property to \"true\". This will let the organizing agent know\nto kick off the organizing task after you're done.\n\nWhen the objective does not explicitly contain the request to organize or summarize results,\nmake sure to set the \"organizeProperty\" to \"false\". Do not invent new work.\n\nNow think real hard: do you need to organize or summarize results?\n`;\n    }\n    async execute(execute, mutableContext, objective, makeList) {\n        const planning = await plannerPrompt(mutableContext, objective, this.extraPlannerPrompt, true).invoke();\n        if (!ok(planning))\n            return planning;\n        let plan = getPlan(planning.last);\n        if (!ok(plan))\n            return plan;\n        const results = [];\n        let max = plan.todo.length + OVERRUN_BUFFER;\n        let organizeResults = false;\n        let planDescription = \"Here is my starting plan\";\n        while (--max) {\n            const task = plan.todo.at(0);\n            if (plan.summarizeResults) {\n                organizeResults = true;\n            }\n            if (!task)\n                break;\n            await report({\n                actor: \"Planner\",\n                category: \"Progress update\",\n                name: \"Thinking\",\n                icon: \"laps\",\n                details: `\n\nHere's my thinking:\n\n${plan.thinking}\n        \n${planDescription}:\n\n${plan.todo.map((item) => `1. ${item.label}`).join(\"\\n\")}\n\nI will now go over the plan in order, thinking after each step\nand adjusting the plan if necessary.`,\n            });\n            const result = await execute(task);\n            if (result) {\n                // Add an 'Action' label so it's easier for the planner to tell what's an agent action vs response.\n                // Unfortunately every item in the context except for function calls has 'role: user'.\n                mutableContext.push(toLLMContent(\"Action: \" + task.task), result);\n                console.log(\"think-strategist\", mutableContext);\n                results.push(result);\n            }\n            this.tasks.push(task.task);\n            const thinking = await thinkingPlannerPrompt(mutableContext, objective, plan, this.tasks, this.extraPlannerPrompt).invoke();\n            if (!ok(thinking))\n                return thinking;\n            const newPlan = getPlan(thinking.last);\n            if (!ok(newPlan))\n                return newPlan;\n            plan = newPlan;\n            planDescription = \"Here are the remaining steps in the plan\";\n        }\n        if (organizeResults) {\n            await report({\n                actor: \"Planner\",\n                category: \"Organizing work into a report\",\n                name: \"Organizing work report\",\n                icon: \"laps\",\n                details: `I will now organize all of my work into a report.`,\n            });\n            const organizing = await organizerPrompt(results, objective, makeList).invoke();\n            if (!ok(organizing))\n                return organizing;\n            return [organizing.last];\n        }\n        return results;\n    }\n}\n",
       "metadata": {
         "title": "conversational-think-strategist",
         "source": {
@@ -159,7 +159,7 @@
       }
     },
     "organizer-prompt": {
-      "code": "/**\n * @fileoverview Plumbing that handles organizing/summarizing content at the end.\n */\nimport { defaultSafetySettings } from \"./a2/gemini\";\nimport { llm, ok, toLLMContent } from \"./a2/utils\";\nimport { GeminiPrompt } from \"./a2/gemini-prompt\";\nimport { StructuredResponse } from \"./a2/structured-response\";\nimport { generateId } from \"./runtime\";\nimport {} from \"./types\";\nimport { listPrompt, listSchema, toList } from \"./a2/lists\";\nexport { organizerPrompt };\nfunction organizerPrompt(results, objective, makeList) {\n    const research = {\n        parts: results.flatMap((item) => item.parts),\n    };\n    console.log(\"RESEARCH\", research);\n    const extra = makeList\n        ? `\nYour job is to examine in detail and organize the provided raw material into\na thorough, detailed list of write-ups, so that the final list of write-ups\nis a perfect response to the objective. The number of write-ups in the list must be the same as\nasked in the objective.\n\nMake sure each write-up is complete as its own document, because these write-ups\nwill be used separately from each other.\n\n\n`\n        : `\nYour job is to examine in detail and organize the provided raw material into\na thorough, detailed write-up that captures all of it in one place, so that\nthe final product is a perfect response to the objective.\n\nThe final must product must contain references to the sources (always cite your sources).`;\n    const prompt = llm `\nYou are an expert organizer of raw material. This raw material was produced by \nan AI agent that was tasked with satisfying the the provided objective.\n\n${extra}\n\n## Objective\n\n${objective}\n\n## Raw Research\n\n\\`\\`\\`\n${research}\n\n\\`\\`\\`\n`.asContent();\n    if (makeList) {\n        const geminiPrompt = new GeminiPrompt({\n            body: {\n                contents: [listPrompt(prompt)],\n                safetySettings: defaultSafetySettings(),\n                generationConfig: {\n                    responseSchema: listSchema(),\n                    responseMimeType: \"application/json\",\n                },\n            },\n        });\n        return {\n            invoke: async () => {\n                const invoking = await geminiPrompt.invoke();\n                if (!ok(invoking))\n                    return invoking;\n                const last = toList(invoking.last);\n                if (!ok(last))\n                    return last;\n                return { ...invoking, last };\n            },\n        };\n    }\n    else {\n        const structuredResponse = new StructuredResponse(generateId(), false);\n        const geminiPrompt = new GeminiPrompt({\n            body: {\n                systemInstruction: structuredResponse.instruction(),\n                contents: structuredResponse.addPrompt([], prompt),\n                safetySettings: defaultSafetySettings(),\n            },\n        }, {\n            validator: (content) => {\n                return structuredResponse.parseContent(content);\n            },\n        });\n        return {\n            invoke: async () => {\n                const invoking = await geminiPrompt.invoke();\n                if (!ok(invoking))\n                    return invoking;\n                const response = toLLMContent(structuredResponse.body, \"model\");\n                return {\n                    ...invoking,\n                    last: response,\n                };\n            },\n        };\n    }\n}\n",
+      "code": "/**\n * @fileoverview Plumbing that handles organizing/summarizing content at the end.\n */\nimport { defaultSafetySettings } from \"./a2/gemini\";\nimport { llm, ok, toLLMContent } from \"./a2/utils\";\nimport { GeminiPrompt } from \"./a2/gemini-prompt\";\nimport { StructuredResponse } from \"./a2/structured-response\";\nimport { generateId } from \"./runtime\";\nimport { listPrompt, listSchema, toList } from \"./a2/lists\";\nexport { organizerPrompt };\nfunction organizerPrompt(results, objective, makeList) {\n    const research = {\n        parts: results.flatMap((item) => item.parts),\n    };\n    console.log(\"RESEARCH\", research);\n    const extra = makeList\n        ? `\nYour job is to examine in detail and organize the provided raw material into\na thorough, detailed list of write-ups, so that the final list of write-ups\nis a perfect response to the objective. The number of write-ups in the list must be the same as\nasked in the objective.\n\nMake sure each write-up is complete as its own document, because these write-ups\nwill be used separately from each other.\n\n\n`\n        : `\nYour job is to examine in detail and organize the provided raw material into\na thorough, detailed write-up that captures all of it in one place, so that\nthe final product is a perfect response to the objective.\n\nThe final must product must contain references to the sources (always cite your sources).`;\n    const prompt = llm `\nYou are an expert organizer of raw material. This raw material was produced by \nan AI agent that was tasked with satisfying the the provided objective.\n\n${extra}\n\n## Objective\n\n${objective}\n\n## Raw Research\n\n\\`\\`\\`\n${research}\n\n\\`\\`\\`\n`.asContent();\n    if (makeList) {\n        const geminiPrompt = new GeminiPrompt({\n            body: {\n                contents: [listPrompt(prompt)],\n                safetySettings: defaultSafetySettings(),\n                generationConfig: {\n                    responseSchema: listSchema(),\n                    responseMimeType: \"application/json\",\n                },\n            },\n        });\n        return {\n            invoke: async () => {\n                const invoking = await geminiPrompt.invoke();\n                if (!ok(invoking))\n                    return invoking;\n                const last = toList(invoking.last);\n                if (!ok(last))\n                    return last;\n                return { ...invoking, last };\n            },\n        };\n    }\n    else {\n        const structuredResponse = new StructuredResponse(generateId(), false);\n        const geminiPrompt = new GeminiPrompt({\n            body: {\n                systemInstruction: structuredResponse.instruction(),\n                contents: structuredResponse.addPrompt([], prompt),\n                safetySettings: defaultSafetySettings(),\n            },\n        }, {\n            validator: (content) => {\n                return structuredResponse.parseContent(content);\n            },\n        });\n        return {\n            invoke: async () => {\n                const invoking = await geminiPrompt.invoke();\n                if (!ok(invoking))\n                    return invoking;\n                const response = toLLMContent(structuredResponse.body, \"model\");\n                return {\n                    ...invoking,\n                    last: response,\n                };\n            },\n        };\n    }\n}\n",
       "metadata": {
         "title": "organizer-prompt",
         "source": {
diff --git a/packages/a2/bgl/google-drive.bgl.json b/packages/a2/bgl/google-drive.bgl.json
index 4dd68100258..7e7f357b76e 100644
--- a/packages/a2/bgl/google-drive.bgl.json
+++ b/packages/a2/bgl/google-drive.bgl.json
@@ -53,7 +53,7 @@
   },
   "modules": {
     "configurator": {
-      "code": "/**\n * @fileoverview Add a description for your module here.\n */\nimport { err, ok } from \"./a2/utils\";\nimport { createConfigurator } from \"./a2/connector-manager\";\nexport { invoke as default, describe };\nconst CONNECTOR_TITLE = \"Google Drive\";\nconst { invoke, describe } = createConfigurator({\n    title: CONNECTOR_TITLE,\n    initialize: async () => {\n        return { title: \"Untitled Drive File\", configuration: {} };\n    },\n    read: async ({ id, configuration }) => {\n        return {\n            schema: {\n                type: \"object\",\n                properties: {\n                    file: {\n                        type: \"object\",\n                        title: \"Google Drive File\",\n                        description: \"Select Google Drive File\",\n                        behavior: [\"google-drive-file-id\"],\n                    },\n                },\n            },\n            values: configuration,\n        };\n    },\n    preview: async ({ configuration }) => {\n        const { id, mimeType } = configuration.file || {};\n        if (!id || !mimeType)\n            return [\n                {\n                    parts: [{ text: \"Untitled Document\" }],\n                },\n            ];\n        return [{ parts: [{ fileData: { fileUri: id, mimeType } }] }];\n    },\n    write: async ({ id, values }) => {\n        console.log(\"WRITE\", id, values);\n        return values;\n    },\n});\n",
+      "code": "/**\n * @fileoverview Add a description for your module here.\n */\nimport { createConfigurator } from \"./a2/connector-manager\";\nexport { invoke as default, describe };\nconst CONNECTOR_TITLE = \"Google Drive\";\nconst { invoke, describe } = createConfigurator({\n    title: CONNECTOR_TITLE,\n    initialize: async () => {\n        return { title: \"Untitled Drive File\", configuration: {} };\n    },\n    read: async ({ id, configuration }) => {\n        return {\n            schema: {\n                type: \"object\",\n                properties: {\n                    file: {\n                        type: \"object\",\n                        title: \"Google Drive File\",\n                        description: \"Select Google Drive File\",\n                        behavior: [\"google-drive-file-id\"],\n                    },\n                },\n            },\n            values: configuration,\n        };\n    },\n    preview: async ({ configuration }) => {\n        const { id, mimeType } = configuration.file || {};\n        if (!id || !mimeType)\n            return [\n                {\n                    parts: [{ text: \"Untitled Document\" }],\n                },\n            ];\n        return [{ parts: [{ fileData: { fileUri: id, mimeType } }] }];\n    },\n    write: async ({ id, values }) => {\n        console.log(\"WRITE\", id, values);\n        return values;\n    },\n});\n",
       "metadata": {
         "title": "configurator",
         "source": {
@@ -85,7 +85,7 @@
       }
     },
     "api": {
-      "code": "/**\n * @license\n * Copyright 2024 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\nimport fetch, {} from \"@fetch\";\nimport secrets from \"@secrets\";\nimport { err, ok } from \"./a2/utils\";\nexport { connect, get, create, del, query, createMultipart, exp, getDoc, updateDoc, createPresentation, getPresentation, updatePresentation, createPermission, };\nconst connectionId = \"connection:$sign-in\";\nasync function get(token, id, metadata) {\n    if (!token) {\n        return err(\"Authentication token is required.\");\n    }\n    if (!id) {\n        return err(\"Please supply file id.\");\n    }\n    return api(metadata, token, `https://www.googleapis.com/drive/v3/files/${id}`, \"GET\");\n}\nasync function create(token, body, metadata) {\n    if (!token) {\n        return err(\"Authentication token is required.\");\n    }\n    if (!body) {\n        return err(\"Please supply the body of the file to create.\");\n    }\n    return api(metadata, token, \"https://www.googleapis.com/drive/v3/files\", \"POST\", body);\n}\nasync function query(token, query, metadata) {\n    if (!token) {\n        return err(\"Authentication token is required.\");\n    }\n    if (!query) {\n        return err(\"Please supply the query.\");\n    }\n    return api(metadata, token, `https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}`, \"GET\");\n}\nasync function del(token, id, metadata) {\n    if (!token) {\n        return err(\"Authentication token is required.\");\n    }\n    if (!id) {\n        return err(\"Please supply the id of the file to delete\");\n    }\n    return api(metadata, token, `https://www.googleapis.com/drive/v3/files/${id}`, \"DELETE\");\n}\nasync function exp(token, fileId, mimeType, metadata) {\n    if (!token) {\n        return err(\"Authentication token is required.\");\n    }\n    if (!fileId) {\n        return err(\"Please supply the file id to export.\");\n    }\n    return api(metadata, token, `https://www.googleapis.com/drive/v3/files/${fileId}/export?mimeType=${mimeType}`, \"GET\");\n}\nasync function getDoc(token, id, metadata) {\n    if (!token) {\n        return err(\"Authentication token is required.\");\n    }\n    if (!id) {\n        return err(\"Please supply the doc id to get.\");\n    }\n    return api(metadata, token, `https://docs.googleapis.com/v1/documents/${id}`, \"GET\");\n}\nasync function updateDoc(token, id, body, metadata) {\n    if (!token) {\n        return err(\"Authentication token is required.\");\n    }\n    if (!id) {\n        return err(\"Please supply the id of the doc to update.\");\n    }\n    if (!body) {\n        return err(\"Please supply the body of the doc update request.\");\n    }\n    return api(metadata, token, `https://docs.googleapis.com/v1/documents/${id}:batchUpdate`, \"POST\", body);\n}\nasync function getPresentation(token, id, metadata) {\n    if (!token) {\n        return err(\"Authentication token is required.\");\n    }\n    return api(metadata, token, `https://slides.googleapis.com/v1/presentations/${id}`, \"GET\");\n}\nasync function createPresentation(token, title, metadata) {\n    if (!token) {\n        return err(\"Authentication token is required.\");\n    }\n    return api(metadata, token, \"https://slides.googleapis.com/v1/presentations\", \"POST\", { title });\n}\nasync function updatePresentation(token, id, body, metadata) {\n    if (!token) {\n        return err(\"Authentication token is required.\");\n    }\n    if (!id) {\n        return err(\"Please supply the id of the presentation to update.\");\n    }\n    if (!body) {\n        return err(\"Please supply the body of the presentation update request.\");\n    }\n    return api(metadata, token, `https://slides.googleapis.com/v1/presentations/${id}:batchUpdate`, \"POST\", body);\n}\nasync function connect(metadata) {\n    const { [connectionId]: token } = await secrets({\n        ...meta(metadata),\n        keys: [connectionId],\n    });\n    return token;\n}\nasync function createMultipart(token, metadata, body, mimeType, $metadata) {\n    const boundary = \"BB-BB-BB-BB-BB-BB\";\n    const url = `https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart`;\n    const request = {\n        ...meta($metadata),\n        url,\n        method: \"POST\",\n        headers: {\n            Authorization: `Bearer ${token}`,\n            [\"Content-Type\"]: `multipart/related; boundary=${boundary}`,\n        },\n        body: `--${boundary}\nContent-Type: application/json; charset=UTF-8\n\n${JSON.stringify(metadata, null, 2)}\n--${boundary}\nContent-Type: ${mimeType}; charset=UTF-8\nContent-Transfer-Encoding: base64\n\n${body}\n--${boundary}--`,\n    };\n    const { response, $error } = await fetch(request);\n    if ($error) {\n        return err(typeof $error === \"string\" ? $error : JSON.stringify($error));\n    }\n    return response;\n}\nasync function createPermission(token, fileId, permission, metadata) {\n    return api(metadata, token, `https://www.googleapis.com/drive/v3/files/${fileId}/permissions`, \"POST\", permission);\n}\nasync function api(metadata, token, url, method, body = null) {\n    const request = {\n        ...meta(metadata),\n        url,\n        method,\n        headers: {\n            Authorization: `Bearer ${token}`,\n        },\n    };\n    if (body) {\n        request.body = body;\n    }\n    const { response, $error } = await fetch(request);\n    if ($error) {\n        return err(typeof $error === \"string\" ? $error : JSON.stringify($error));\n    }\n    return response;\n}\nfunction meta({ title, description } = {}) {\n    if (!(title || description))\n        return {};\n    const $metadata = {};\n    if (title) {\n        $metadata.title = title;\n    }\n    if (description) {\n        $metadata.description = description;\n    }\n    return { $metadata };\n}\n",
+      "code": "/**\n * @license\n * Copyright 2024 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\nimport fetch from \"@fetch\";\nimport secrets from \"@secrets\";\nimport { err } from \"./a2/utils\";\nexport { connect, get, create, del, query, createMultipart, exp, getDoc, updateDoc, createPresentation, getPresentation, updatePresentation, createPermission, };\nconst connectionId = \"connection:$sign-in\";\nasync function get(token, id, metadata) {\n    if (!token) {\n        return err(\"Authentication token is required.\");\n    }\n    if (!id) {\n        return err(\"Please supply file id.\");\n    }\n    return api(metadata, token, `https://www.googleapis.com/drive/v3/files/${id}`, \"GET\");\n}\nasync function create(token, body, metadata) {\n    if (!token) {\n        return err(\"Authentication token is required.\");\n    }\n    if (!body) {\n        return err(\"Please supply the body of the file to create.\");\n    }\n    return api(metadata, token, \"https://www.googleapis.com/drive/v3/files\", \"POST\", body);\n}\nasync function query(token, query, metadata) {\n    if (!token) {\n        return err(\"Authentication token is required.\");\n    }\n    if (!query) {\n        return err(\"Please supply the query.\");\n    }\n    return api(metadata, token, `https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}`, \"GET\");\n}\nasync function del(token, id, metadata) {\n    if (!token) {\n        return err(\"Authentication token is required.\");\n    }\n    if (!id) {\n        return err(\"Please supply the id of the file to delete\");\n    }\n    return api(metadata, token, `https://www.googleapis.com/drive/v3/files/${id}`, \"DELETE\");\n}\nasync function exp(token, fileId, mimeType, metadata) {\n    if (!token) {\n        return err(\"Authentication token is required.\");\n    }\n    if (!fileId) {\n        return err(\"Please supply the file id to export.\");\n    }\n    return api(metadata, token, `https://www.googleapis.com/drive/v3/files/${fileId}/export?mimeType=${mimeType}`, \"GET\");\n}\nasync function getDoc(token, id, metadata) {\n    if (!token) {\n        return err(\"Authentication token is required.\");\n    }\n    if (!id) {\n        return err(\"Please supply the doc id to get.\");\n    }\n    return api(metadata, token, `https://docs.googleapis.com/v1/documents/${id}`, \"GET\");\n}\nasync function updateDoc(token, id, body, metadata) {\n    if (!token) {\n        return err(\"Authentication token is required.\");\n    }\n    if (!id) {\n        return err(\"Please supply the id of the doc to update.\");\n    }\n    if (!body) {\n        return err(\"Please supply the body of the doc update request.\");\n    }\n    return api(metadata, token, `https://docs.googleapis.com/v1/documents/${id}:batchUpdate`, \"POST\", body);\n}\nasync function getPresentation(token, id, metadata) {\n    if (!token) {\n        return err(\"Authentication token is required.\");\n    }\n    return api(metadata, token, `https://slides.googleapis.com/v1/presentations/${id}`, \"GET\");\n}\nasync function createPresentation(token, title, metadata) {\n    if (!token) {\n        return err(\"Authentication token is required.\");\n    }\n    return api(metadata, token, \"https://slides.googleapis.com/v1/presentations\", \"POST\", { title });\n}\nasync function updatePresentation(token, id, body, metadata) {\n    if (!token) {\n        return err(\"Authentication token is required.\");\n    }\n    if (!id) {\n        return err(\"Please supply the id of the presentation to update.\");\n    }\n    if (!body) {\n        return err(\"Please supply the body of the presentation update request.\");\n    }\n    return api(metadata, token, `https://slides.googleapis.com/v1/presentations/${id}:batchUpdate`, \"POST\", body);\n}\nasync function connect(metadata) {\n    const { [connectionId]: token } = await secrets({\n        ...meta(metadata),\n        keys: [connectionId],\n    });\n    return token;\n}\nasync function createMultipart(token, metadata, body, mimeType, $metadata) {\n    const boundary = \"BB-BB-BB-BB-BB-BB\";\n    const url = `https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart`;\n    const request = {\n        ...meta($metadata),\n        url,\n        method: \"POST\",\n        headers: {\n            Authorization: `Bearer ${token}`,\n            [\"Content-Type\"]: `multipart/related; boundary=${boundary}`,\n        },\n        body: `--${boundary}\nContent-Type: application/json; charset=UTF-8\n\n${JSON.stringify(metadata, null, 2)}\n--${boundary}\nContent-Type: ${mimeType}; charset=UTF-8\nContent-Transfer-Encoding: base64\n\n${body}\n--${boundary}--`,\n    };\n    const { response, $error } = await fetch(request);\n    if ($error) {\n        return err(typeof $error === \"string\" ? $error : JSON.stringify($error));\n    }\n    return response;\n}\nasync function createPermission(token, fileId, permission, metadata) {\n    return api(metadata, token, `https://www.googleapis.com/drive/v3/files/${fileId}/permissions`, \"POST\", permission);\n}\nasync function api(metadata, token, url, method, body = null) {\n    const request = {\n        ...meta(metadata),\n        url,\n        method,\n        headers: {\n            Authorization: `Bearer ${token}`,\n        },\n    };\n    if (body) {\n        request.body = body;\n    }\n    const { response, $error } = await fetch(request);\n    if ($error) {\n        return err(typeof $error === \"string\" ? $error : JSON.stringify($error));\n    }\n    return response;\n}\nfunction meta({ title, description } = {}) {\n    if (!(title || description))\n        return {};\n    const $metadata = {};\n    if (title) {\n        $metadata.title = title;\n    }\n    if (description) {\n        $metadata.description = description;\n    }\n    return { $metadata };\n}\n",
       "metadata": {
         "title": "api",
         "source": {
@@ -97,7 +97,7 @@
       }
     },
     "slides": {
-      "code": "/**\n * @fileoverview Slides bits.\n */\nimport { unescape } from \"./unescape\";\nimport { marked } from \"./marked\";\nexport { SlideBuilder, slidesToRequests };\nclass SlideBuilder {\n    #slides = [];\n    #images = [];\n    #startIndex;\n    #objectToDelete;\n    #depthAdjustment = 0;\n    constructor(startIndex = 0, objectId) {\n        this.#startIndex = startIndex;\n        this.#objectToDelete = objectId;\n        this.#newSlide();\n    }\n    addMarkdown(markdown) {\n        let bodyText = this.#getBodyText();\n        if (this.#slide.title || bodyText.text || bodyText.images?.length) {\n            this.#newSlide();\n        }\n        const tokens = marked.lexer(markdown);\n        tokens.forEach((token) => this.#addToken(token));\n    }\n    #hasContent() { }\n    addInlineData(data) {\n        let bodyText = this.#getBodyText();\n        if (this.#slide.title || bodyText.text || bodyText.images?.length) {\n            this.#newSlide();\n            bodyText = this.#getBodyText();\n        }\n        bodyText.images ??= [];\n        bodyText.images.push(this.#addImage({\n            type: \"image\",\n            href: `data:${data.mimeType};base64,${data.data}`,\n        }));\n    }\n    #addToken(token) {\n        const { type } = token;\n        switch (type) {\n            case \"hr\":\n                this.#newSlide();\n                break;\n            case \"paragraph\":\n                this.#newParagraph(token.tokens);\n                break;\n            case \"heading\":\n                this.#newHeading(token);\n                break;\n            case \"list\":\n                this.#addListToBody(token);\n                break;\n        }\n    }\n    images() {\n        return this.#images;\n    }\n    build(imageUrls) {\n        this.#finalizeSlide();\n        console.log(\"SLIDES\", this.#slides);\n        const requests = slidesToRequests(this.#slides, imageUrls);\n        if (this.#objectToDelete) {\n            requests.unshift({\n                deleteObject: {\n                    objectId: this.#objectToDelete,\n                },\n            });\n        }\n        return requests;\n    }\n    get #slide() {\n        return this.#slides.at(-1);\n    }\n    #newHeading(token) {\n        if (token.depth === 1) {\n            this.#slide.title = this.#parseText(token.tokens);\n        }\n        else {\n            this.#slide.subtitle = this.#parseText(token.tokens);\n        }\n    }\n    #newParagraph(tokens) {\n        const bodyText = this.#getBodyText();\n        const offset = bodyText.text.length - this.#depthAdjustment;\n        const { text, styles, lists, images = [], } = this.#parseText(tokens, offset);\n        bodyText.text += text;\n        bodyText.styles.push(...styles);\n        bodyText.lists.push(...lists);\n        bodyText.images ??= [];\n        bodyText.images.push(...images);\n    }\n    #getBodyText() {\n        const slide = this.#slide;\n        if (!slide.body.length) {\n            slide.body.push({\n                text: { text: \"\", styles: [], lists: [] },\n            });\n        }\n        const body = slide.body.at(-1);\n        if (!body.text) {\n            body.text = { text: \"\", styles: [], lists: [] };\n        }\n        return body.text;\n    }\n    #addListToBody(token) {\n        const slide = this.#slide;\n        const { ordered, items } = token;\n        let bulletedText = \"\";\n        const bodyText = this.#getBodyText();\n        let listOffset = bodyText.text.length;\n        let length = 0;\n        let localOffset = listOffset;\n        const addListItems = (depth, items) => {\n            items.forEach((item) => {\n                const [textToken, listToken] = item.tokens;\n                const { text, styles } = this.#parseText(textToken.tokens, localOffset);\n                bodyText.text += `${\"\\t\".repeat(depth)}${text}`;\n                this.#depthAdjustment += depth;\n                localOffset += text.length;\n                length += text.length;\n                bodyText.styles.push(...styles);\n                if (listToken) {\n                    addListItems(depth + 1, listToken.items);\n                }\n            });\n        };\n        addListItems(0, items);\n        bodyText.lists.push({ start: listOffset, end: listOffset + length });\n    }\n    #parseText(tokens, current = 0) {\n        let text = \"\";\n        const styles = [];\n        const images = [];\n        tokens.forEach((token) => {\n            if (token.type === \"image\") {\n                images.push(this.#addImage(token));\n                return;\n            }\n            const { type, text: t } = token;\n            const length = t.length;\n            text += unescape(t);\n            const range = { start: current, end: current + length };\n            switch (type) {\n                case \"strong\":\n                    styles.push({ range, bold: true });\n                    break;\n                case \"em\":\n                    styles.push({ range, italic: true });\n                    break;\n                case \"del\":\n                    styles.push({ range, strikethrough: true });\n                    break;\n                case \"link\":\n                    styles.push({ range, link: token.href });\n                    break;\n            }\n            current += length;\n        });\n        return { text: `${text}\\n`, styles, lists: [], images };\n    }\n    #finalizeSlide() {\n        const slide = this.#slide;\n        if (!slide)\n            return;\n        const hasText = !!slide.body?.at(0)?.text?.text;\n        const hasImages = !!slide.body?.at(0)?.text?.images?.length;\n        if (slide.subtitle && !hasText) {\n            slide.layout = \"TITLE\";\n        }\n        else if (hasText) {\n            slide.layout = \"TITLE_AND_BODY\";\n            delete slide.subtitle;\n        }\n        else if (!hasImages) {\n            slide.layout = \"MAIN_POINT\";\n            slide.body = [];\n        }\n        this.#depthAdjustment = 0;\n    }\n    #newSlide() {\n        this.#finalizeSlide();\n        this.#slides.push({\n            objectId: `Slide-${this.#startIndex + this.#slides.length}`,\n            layout: \"BLANK\",\n            body: [],\n        });\n    }\n    #addImage(token) {\n        const id = this.#images.length;\n        this.#images.push(token);\n        return id;\n    }\n}\nfunction slidesToRequests(slides, imageUrls) {\n    const requests = [];\n    slides.forEach((slide) => {\n        const request = {\n            objectId: slide.objectId,\n            slideLayoutReference: { predefinedLayout: slide.layout },\n            placeholderIdMappings: mapPlaceholders(slide.objectId, slide.layout),\n        };\n        requests.push({ createSlide: request });\n        if (slide.title) {\n            requests.push({\n                insertText: {\n                    text: slide.title.text,\n                    objectId: `${slide.objectId}-title`,\n                },\n            });\n        }\n        if (slide.subtitle) {\n            requests.push({\n                insertText: {\n                    text: slide.subtitle.text,\n                    objectId: `${slide.objectId}-subtitle`,\n                },\n            });\n        }\n        slide.body.forEach((body) => {\n            const bodyText = body.text;\n            if (!bodyText)\n                return;\n            if (bodyText.images?.length) {\n                requests.push({\n                    createImage: {\n                        url: imageUrls[bodyText.images[0]],\n                        elementProperties: {\n                            pageObjectId: slide.objectId,\n                        },\n                    },\n                });\n            }\n            else if (bodyText.text) {\n                const objectId = `${slide.objectId}-body`;\n                requests.push({\n                    insertText: { text: bodyText.text, objectId },\n                });\n                bodyText.lists.forEach((list) => {\n                    requests.push({\n                        createParagraphBullets: {\n                            objectId,\n                            textRange: {\n                                type: \"FIXED_RANGE\",\n                                startIndex: list.start,\n                                endIndex: list.end,\n                            },\n                        },\n                    });\n                });\n                bodyText.styles.forEach((style) => {\n                    requests.push({\n                        updateTextStyle: {\n                            objectId,\n                            ...getTextStyle(style),\n                            textRange: {\n                                type: \"FIXED_RANGE\",\n                                startIndex: style.range.start,\n                                endIndex: style.range.end,\n                            },\n                        },\n                    });\n                });\n            }\n        });\n    });\n    return requests;\n}\nfunction getTextStyle(style) {\n    const { link: url, range: _, ...rest } = style;\n    const linkStyle = url ? { link: { url } } : {};\n    const fields = Object.keys(rest);\n    if (url)\n        fields.push(\"link\");\n    return { style: { ...linkStyle, ...rest }, fields: fields.join(\",\") };\n}\nfunction mapPlaceholders(slideId, layout) {\n    const mappings = [];\n    switch (layout) {\n        case \"TITLE\":\n            mappings.push({\n                layoutPlaceholder: { type: \"CENTERED_TITLE\", index: 0 },\n                objectId: `${slideId}-title`,\n            });\n            mappings.push({\n                layoutPlaceholder: { type: \"SUBTITLE\", index: 0 },\n                objectId: `${slideId}-subtitle`,\n            });\n            break;\n        case \"TITLE_AND_BODY\":\n            mappings.push({\n                layoutPlaceholder: { type: \"TITLE\", index: 0 },\n                objectId: `${slideId}-title`,\n            });\n            mappings.push({\n                layoutPlaceholder: { type: \"BODY\", index: 0 },\n                objectId: `${slideId}-body`,\n            });\n            break;\n        case \"MAIN_POINT\":\n            mappings.push({\n                layoutPlaceholder: { type: \"TITLE\", index: 0 },\n                objectId: `${slideId}-title`,\n            });\n            break;\n    }\n    return mappings;\n}\n",
+      "code": "/**\n * @fileoverview Slides bits.\n */\nvar __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {\n    if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\n    return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\n};\nvar __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n    return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n};\nvar _SlideBuilder_instances, _SlideBuilder_slides, _SlideBuilder_images, _SlideBuilder_startIndex, _SlideBuilder_objectToDelete, _SlideBuilder_depthAdjustment, _SlideBuilder_hasContent, _SlideBuilder_addToken, _SlideBuilder_slide_get, _SlideBuilder_newHeading, _SlideBuilder_newParagraph, _SlideBuilder_getBodyText, _SlideBuilder_addListToBody, _SlideBuilder_parseText, _SlideBuilder_finalizeSlide, _SlideBuilder_newSlide, _SlideBuilder_addImage;\nimport { unescape } from \"./unescape\";\nimport { marked } from \"./marked\";\nexport { SlideBuilder, slidesToRequests };\nclass SlideBuilder {\n    constructor(startIndex = 0, objectId) {\n        _SlideBuilder_instances.add(this);\n        _SlideBuilder_slides.set(this, []);\n        _SlideBuilder_images.set(this, []);\n        _SlideBuilder_startIndex.set(this, void 0);\n        _SlideBuilder_objectToDelete.set(this, void 0);\n        _SlideBuilder_depthAdjustment.set(this, 0);\n        __classPrivateFieldSet(this, _SlideBuilder_startIndex, startIndex, \"f\");\n        __classPrivateFieldSet(this, _SlideBuilder_objectToDelete, objectId, \"f\");\n        __classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_newSlide).call(this);\n    }\n    addMarkdown(markdown) {\n        let bodyText = __classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_getBodyText).call(this);\n        if (__classPrivateFieldGet(this, _SlideBuilder_instances, \"a\", _SlideBuilder_slide_get).title || bodyText.text || bodyText.images?.length) {\n            __classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_newSlide).call(this);\n        }\n        const tokens = marked.lexer(markdown);\n        tokens.forEach((token) => __classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_addToken).call(this, token));\n    }\n    addInlineData(data) {\n        let bodyText = __classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_getBodyText).call(this);\n        if (__classPrivateFieldGet(this, _SlideBuilder_instances, \"a\", _SlideBuilder_slide_get).title || bodyText.text || bodyText.images?.length) {\n            __classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_newSlide).call(this);\n            bodyText = __classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_getBodyText).call(this);\n        }\n        bodyText.images ?? (bodyText.images = []);\n        bodyText.images.push(__classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_addImage).call(this, {\n            type: \"image\",\n            href: `data:${data.mimeType};base64,${data.data}`,\n        }));\n    }\n    images() {\n        return __classPrivateFieldGet(this, _SlideBuilder_images, \"f\");\n    }\n    build(imageUrls) {\n        __classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_finalizeSlide).call(this);\n        console.log(\"SLIDES\", __classPrivateFieldGet(this, _SlideBuilder_slides, \"f\"));\n        const requests = slidesToRequests(__classPrivateFieldGet(this, _SlideBuilder_slides, \"f\"), imageUrls);\n        if (__classPrivateFieldGet(this, _SlideBuilder_objectToDelete, \"f\")) {\n            requests.unshift({\n                deleteObject: {\n                    objectId: __classPrivateFieldGet(this, _SlideBuilder_objectToDelete, \"f\"),\n                },\n            });\n        }\n        return requests;\n    }\n}\n_SlideBuilder_slides = new WeakMap(), _SlideBuilder_images = new WeakMap(), _SlideBuilder_startIndex = new WeakMap(), _SlideBuilder_objectToDelete = new WeakMap(), _SlideBuilder_depthAdjustment = new WeakMap(), _SlideBuilder_instances = new WeakSet(), _SlideBuilder_hasContent = function _SlideBuilder_hasContent() { }, _SlideBuilder_addToken = function _SlideBuilder_addToken(token) {\n    const { type } = token;\n    switch (type) {\n        case \"hr\":\n            __classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_newSlide).call(this);\n            break;\n        case \"paragraph\":\n            __classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_newParagraph).call(this, token.tokens);\n            break;\n        case \"heading\":\n            __classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_newHeading).call(this, token);\n            break;\n        case \"list\":\n            __classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_addListToBody).call(this, token);\n            break;\n    }\n}, _SlideBuilder_slide_get = function _SlideBuilder_slide_get() {\n    return __classPrivateFieldGet(this, _SlideBuilder_slides, \"f\").at(-1);\n}, _SlideBuilder_newHeading = function _SlideBuilder_newHeading(token) {\n    if (token.depth === 1) {\n        __classPrivateFieldGet(this, _SlideBuilder_instances, \"a\", _SlideBuilder_slide_get).title = __classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_parseText).call(this, token.tokens);\n    }\n    else {\n        __classPrivateFieldGet(this, _SlideBuilder_instances, \"a\", _SlideBuilder_slide_get).subtitle = __classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_parseText).call(this, token.tokens);\n    }\n}, _SlideBuilder_newParagraph = function _SlideBuilder_newParagraph(tokens) {\n    const bodyText = __classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_getBodyText).call(this);\n    const offset = bodyText.text.length - __classPrivateFieldGet(this, _SlideBuilder_depthAdjustment, \"f\");\n    const { text, styles, lists, images = [], } = __classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_parseText).call(this, tokens, offset);\n    bodyText.text += text;\n    bodyText.styles.push(...styles);\n    bodyText.lists.push(...lists);\n    bodyText.images ?? (bodyText.images = []);\n    bodyText.images.push(...images);\n}, _SlideBuilder_getBodyText = function _SlideBuilder_getBodyText() {\n    const slide = __classPrivateFieldGet(this, _SlideBuilder_instances, \"a\", _SlideBuilder_slide_get);\n    if (!slide.body.length) {\n        slide.body.push({\n            text: { text: \"\", styles: [], lists: [] },\n        });\n    }\n    const body = slide.body.at(-1);\n    if (!body.text) {\n        body.text = { text: \"\", styles: [], lists: [] };\n    }\n    return body.text;\n}, _SlideBuilder_addListToBody = function _SlideBuilder_addListToBody(token) {\n    const slide = __classPrivateFieldGet(this, _SlideBuilder_instances, \"a\", _SlideBuilder_slide_get);\n    const { ordered, items } = token;\n    let bulletedText = \"\";\n    const bodyText = __classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_getBodyText).call(this);\n    let listOffset = bodyText.text.length;\n    let length = 0;\n    let localOffset = listOffset;\n    const addListItems = (depth, items) => {\n        items.forEach((item) => {\n            const [textToken, listToken] = item.tokens;\n            const { text, styles } = __classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_parseText).call(this, textToken.tokens, localOffset);\n            bodyText.text += `${\"\\t\".repeat(depth)}${text}`;\n            __classPrivateFieldSet(this, _SlideBuilder_depthAdjustment, __classPrivateFieldGet(this, _SlideBuilder_depthAdjustment, \"f\") + depth, \"f\");\n            localOffset += text.length;\n            length += text.length;\n            bodyText.styles.push(...styles);\n            if (listToken) {\n                addListItems(depth + 1, listToken.items);\n            }\n        });\n    };\n    addListItems(0, items);\n    bodyText.lists.push({ start: listOffset, end: listOffset + length });\n}, _SlideBuilder_parseText = function _SlideBuilder_parseText(tokens, current = 0) {\n    let text = \"\";\n    const styles = [];\n    const images = [];\n    tokens.forEach((token) => {\n        if (token.type === \"image\") {\n            images.push(__classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_addImage).call(this, token));\n            return;\n        }\n        const { type, text: t } = token;\n        const length = t.length;\n        text += unescape(t);\n        const range = { start: current, end: current + length };\n        switch (type) {\n            case \"strong\":\n                styles.push({ range, bold: true });\n                break;\n            case \"em\":\n                styles.push({ range, italic: true });\n                break;\n            case \"del\":\n                styles.push({ range, strikethrough: true });\n                break;\n            case \"link\":\n                styles.push({ range, link: token.href });\n                break;\n        }\n        current += length;\n    });\n    return { text: `${text}\\n`, styles, lists: [], images };\n}, _SlideBuilder_finalizeSlide = function _SlideBuilder_finalizeSlide() {\n    const slide = __classPrivateFieldGet(this, _SlideBuilder_instances, \"a\", _SlideBuilder_slide_get);\n    if (!slide)\n        return;\n    const hasText = !!slide.body?.at(0)?.text?.text;\n    const hasImages = !!slide.body?.at(0)?.text?.images?.length;\n    if (slide.subtitle && !hasText) {\n        slide.layout = \"TITLE\";\n    }\n    else if (hasText) {\n        slide.layout = \"TITLE_AND_BODY\";\n        delete slide.subtitle;\n    }\n    else if (!hasImages) {\n        slide.layout = \"MAIN_POINT\";\n        slide.body = [];\n    }\n    __classPrivateFieldSet(this, _SlideBuilder_depthAdjustment, 0, \"f\");\n}, _SlideBuilder_newSlide = function _SlideBuilder_newSlide() {\n    __classPrivateFieldGet(this, _SlideBuilder_instances, \"m\", _SlideBuilder_finalizeSlide).call(this);\n    __classPrivateFieldGet(this, _SlideBuilder_slides, \"f\").push({\n        objectId: `Slide-${__classPrivateFieldGet(this, _SlideBuilder_startIndex, \"f\") + __classPrivateFieldGet(this, _SlideBuilder_slides, \"f\").length}`,\n        layout: \"BLANK\",\n        body: [],\n    });\n}, _SlideBuilder_addImage = function _SlideBuilder_addImage(token) {\n    const id = __classPrivateFieldGet(this, _SlideBuilder_images, \"f\").length;\n    __classPrivateFieldGet(this, _SlideBuilder_images, \"f\").push(token);\n    return id;\n};\nfunction slidesToRequests(slides, imageUrls) {\n    const requests = [];\n    slides.forEach((slide) => {\n        const request = {\n            objectId: slide.objectId,\n            slideLayoutReference: { predefinedLayout: slide.layout },\n            placeholderIdMappings: mapPlaceholders(slide.objectId, slide.layout),\n        };\n        requests.push({ createSlide: request });\n        if (slide.title) {\n            requests.push({\n                insertText: {\n                    text: slide.title.text,\n                    objectId: `${slide.objectId}-title`,\n                },\n            });\n        }\n        if (slide.subtitle) {\n            requests.push({\n                insertText: {\n                    text: slide.subtitle.text,\n                    objectId: `${slide.objectId}-subtitle`,\n                },\n            });\n        }\n        slide.body.forEach((body) => {\n            const bodyText = body.text;\n            if (!bodyText)\n                return;\n            if (bodyText.images?.length) {\n                requests.push({\n                    createImage: {\n                        url: imageUrls[bodyText.images[0]],\n                        elementProperties: {\n                            pageObjectId: slide.objectId,\n                        },\n                    },\n                });\n            }\n            else if (bodyText.text) {\n                const objectId = `${slide.objectId}-body`;\n                requests.push({\n                    insertText: { text: bodyText.text, objectId },\n                });\n                bodyText.lists.forEach((list) => {\n                    requests.push({\n                        createParagraphBullets: {\n                            objectId,\n                            textRange: {\n                                type: \"FIXED_RANGE\",\n                                startIndex: list.start,\n                                endIndex: list.end,\n                            },\n                        },\n                    });\n                });\n                bodyText.styles.forEach((style) => {\n                    requests.push({\n                        updateTextStyle: {\n                            objectId,\n                            ...getTextStyle(style),\n                            textRange: {\n                                type: \"FIXED_RANGE\",\n                                startIndex: style.range.start,\n                                endIndex: style.range.end,\n                            },\n                        },\n                    });\n                });\n            }\n        });\n    });\n    return requests;\n}\nfunction getTextStyle(style) {\n    const { link: url, range: _, ...rest } = style;\n    const linkStyle = url ? { link: { url } } : {};\n    const fields = Object.keys(rest);\n    if (url)\n        fields.push(\"link\");\n    return { style: { ...linkStyle, ...rest }, fields: fields.join(\",\") };\n}\nfunction mapPlaceholders(slideId, layout) {\n    const mappings = [];\n    switch (layout) {\n        case \"TITLE\":\n            mappings.push({\n                layoutPlaceholder: { type: \"CENTERED_TITLE\", index: 0 },\n                objectId: `${slideId}-title`,\n            });\n            mappings.push({\n                layoutPlaceholder: { type: \"SUBTITLE\", index: 0 },\n                objectId: `${slideId}-subtitle`,\n            });\n            break;\n        case \"TITLE_AND_BODY\":\n            mappings.push({\n                layoutPlaceholder: { type: \"TITLE\", index: 0 },\n                objectId: `${slideId}-title`,\n            });\n            mappings.push({\n                layoutPlaceholder: { type: \"BODY\", index: 0 },\n                objectId: `${slideId}-body`,\n            });\n            break;\n        case \"MAIN_POINT\":\n            mappings.push({\n                layoutPlaceholder: { type: \"TITLE\", index: 0 },\n                objectId: `${slideId}-title`,\n            });\n            break;\n    }\n    return mappings;\n}\n",
       "metadata": {
         "title": "slides",
         "source": {
@@ -109,7 +109,7 @@
       }
     },
     "docs": {
-      "code": "import { marked } from \"./marked\";\nimport { unescape } from \"./unescape\";\nimport { toText, mergeTextParts } from \"./a2/utils\";\nimport transformBlob from \"@blob\";\nexport { contextToRequests, markdownToContext, DOC_MIME_TYPE };\nconst DOC_MIME_TYPE = \"application/vnd.google-apps.document\";\n// async function transformBlob(args: {\n//   contents: LLMContent[];\n//   transform: string;\n// }): Promise<{ contents: LLMContent[] }> {\n//   return args;\n// }\n/**\n * Removes surrounding backticks from each line if each\n * line is surrounded by backticks.\n * This is a bug in Google Drive markdown conversion.\n * Here's hoping it won't do too much damage.\n */\nfunction sanitizeBackticks(s) {\n    return s\n        .split(\"\\n\")\n        .map((line) => {\n        const trimmed = line.trim();\n        if (trimmed.length === 0)\n            return line;\n        if (trimmed.startsWith(\"`\") && trimmed.endsWith(\"`\")) {\n            return trimmed.slice(1, -1);\n        }\n        return line;\n    })\n        .join(\"\\n\");\n}\nconst BASE64_DATA_URL_REGEX = /^data:(.+?);base64,(.+)$/;\nfunction parseBase64DataUrl(url) {\n    const matchResult = url.match(BASE64_DATA_URL_REGEX);\n    if (!matchResult || matchResult.length !== 3) {\n        return null;\n    }\n    const [, mimeType, data] = matchResult;\n    return { inlineData: { mimeType, data } };\n}\nfunction markdownToContext(markdown) {\n    const tokens = marked.lexer(sanitizeBackticks(markdown));\n    const parts = mergeTextParts(tokens.flatMap((token) => {\n        if (token.type === \"paragraph\") {\n            return token.tokens.map((token) => {\n                if (token.type === \"image\") {\n                    const inlineData = parseBase64DataUrl(token.href);\n                    if (inlineData)\n                        return inlineData;\n                }\n                return { text: token.raw };\n            });\n        }\n        return { text: token.raw };\n    }));\n    return [{ parts }];\n}\nasync function contextToRequests(context, startIndex) {\n    const parts = context?.at(-1)?.parts;\n    if (!parts)\n        return [];\n    const result = [];\n    let index = startIndex;\n    for (const part of parts) {\n        if (\"text\" in part) {\n            const tokens = marked.lexer(toText(context));\n            const { lastIndex, requests } = tokensToRequests(tokens, index);\n            result.push(...requests);\n            index = lastIndex;\n        }\n        else if (\"inlineData\" in part) {\n            const contents = await transformBlob({\n                contents: [{ parts: [part] }],\n                transform: \"persistent-temporary\",\n            });\n            const storedPart = contents?.contents\n                ?.at(0)\n                ?.parts?.at(0);\n            if (storedPart) {\n                result.push({\n                    insertInlineImage: {\n                        uri: storedPart.storedData.handle,\n                        location: {\n                            index,\n                        },\n                    },\n                });\n            }\n        }\n    }\n    return result;\n}\n/**\n * Converts markdown tokens to Google Doc Request array for the\n * `batchUpdate` call.\n */\nfunction tokensToRequests(tokens, startIndex) {\n    let current = startIndex;\n    const requests = tokens.flatMap((token) => {\n        switch (token.type) {\n            case \"paragraph\":\n                return insertFormattedText(token, \"NORMAL_TEXT\");\n            case \"space\":\n                return insertSpace(token);\n            case \"code\":\n                return insertFormattedText(token, \"NORMAL_TEXT\");\n            case \"heading\":\n                return insertFormattedText(token, `HEADING_${token.depth}`);\n            case \"blockquote\":\n                return insertFormattedText(token, \"NORMAL_TEXT\");\n            case \"list\":\n                return insertList(token.items, token.ordered, 0);\n        }\n        return [];\n    });\n    return { lastIndex: current, requests };\n    function insertFormattedText(token, namedStyleType) {\n        const { requests, text: withoutBreak } = new TextStyles(current, token).parse();\n        const text = `${withoutBreak}\\n`;\n        if (namedStyleType) {\n            requests.unshift({\n                updateParagraphStyle: {\n                    range: range(text.length),\n                    paragraphStyle: { namedStyleType },\n                    fields: \"namedStyleType\",\n                },\n            });\n        }\n        requests.unshift({ insertText: { text, location: location() } });\n        current += text.length;\n        return requests;\n    }\n    function insertSpace(token) {\n        const text = token.raw.startsWith(\"\\n\") ? token.raw.slice(1) : token.raw;\n        const result = [\n            {\n                insertText: { text, location: location() },\n            },\n        ];\n        return advance(result, text.length);\n    }\n    function range(length) {\n        return { startIndex: current, endIndex: current + length };\n    }\n    function location() {\n        return { index: current };\n    }\n    function insertList(items, ordered, depth) {\n        const start = current;\n        // This is necessary to counteract a gnarly side-effect of creating a\n        // bullet list: the indent markers are being removed during that change,\n        // and change all of the ranges. So we have to make sure that the next\n        // request accounts for that.\n        let depthToRemove = 0;\n        const list = descendIntoList(items, ordered, depth).flat();\n        list.push({\n            createParagraphBullets: {\n                range: { startIndex: start, endIndex: current },\n                bulletPreset: \"BULLET_DISC_CIRCLE_SQUARE\",\n            },\n        });\n        current -= depthToRemove;\n        return list;\n        function descendIntoList(items, ordered, depth) {\n            const list = items.flatMap((item) => {\n                // For item, item.type === \"list_item\".\n                const indent = \"\\t\".repeat(depth);\n                depthToRemove += depth;\n                const result = [];\n                const maybeRichToken = \"tokens\" in item ? item : undefined;\n                const subList = maybeRichToken?.tokens.find((token) => token?.type === \"list\");\n                if (subList) {\n                    // assume that the first token is actually the text token.\n                    result.push(insertItemText(indent, maybeRichToken?.tokens.at(0)));\n                    result.push(...descendIntoList(subList.items, ordered, depth + 1));\n                }\n                else {\n                    result.push(insertItemText(indent, maybeRichToken?.tokens.at(0)));\n                }\n                return result;\n            });\n            return list;\n        }\n        function insertItemText(indent, token) {\n            const offset = current + indent.length;\n            const { requests, text: withoutIndent } = new TextStyles(offset, token).parse();\n            const text = `${indent}${withoutIndent}\\n`;\n            requests.unshift({\n                updateParagraphStyle: {\n                    range: range(text.length),\n                    paragraphStyle: { namedStyleType: \"NORMAL_TEXT\" },\n                    fields: \"namedStyleType\",\n                },\n            });\n            requests.unshift({ insertText: { text, location: location() } });\n            current += text.length;\n            return requests;\n        }\n    }\n    function advance(result, length) {\n        current += length;\n        return result;\n    }\n}\nclass TextStyles {\n    #offset;\n    #tokens = [];\n    #styles = [];\n    constructor(offset, token) {\n        this.#offset = offset;\n        if (token && \"tokens\" in token) {\n            this.#tokens = token.tokens;\n        }\n    }\n    range(startIndex, length) {\n        return { startIndex, endIndex: startIndex + length };\n    }\n    style(startIndex, length, textStyle) {\n        this.#styles.push({\n            updateTextStyle: {\n                range: this.range(startIndex, length),\n                textStyle,\n                fields: Object.keys(textStyle).join(\",\"),\n            },\n        });\n    }\n    parse() {\n        let current = this.#offset;\n        let text = \"\";\n        for (let token of this.#tokens) {\n            let tokenText = unescape(\"text\" in token ? token.text : \"\");\n            const length = tokenText.length;\n            text += tokenText;\n            switch (token.type) {\n                case \"strong\":\n                    this.style(current, length, { bold: true });\n                    break;\n                case \"em\":\n                    this.style(current, length, { italic: true });\n                    break;\n                case \"codespan\":\n                    this.style(current, length, {\n                        weightedFontFamily: {\n                            fontFamily: \"Fira Code\",\n                        },\n                    });\n                    break;\n                case \"del\":\n                    this.style(current, length, { strikethrough: true });\n                    break;\n                case \"link\":\n                    this.style(current, length, { link: { url: token.href } });\n                    break;\n                case \"escape\":\n                    console.log(\"ESCAPE\", token);\n                    break;\n            }\n            current += length;\n        }\n        return {\n            requests: this.#styles,\n            text,\n        };\n    }\n}\n",
+      "code": "var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {\n    if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\n    return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\n};\nvar __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n    return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n};\nvar _TextStyles_offset, _TextStyles_tokens, _TextStyles_styles;\nimport { marked } from \"./marked\";\nimport { unescape } from \"./unescape\";\nimport { toText, mergeTextParts } from \"./a2/utils\";\nimport transformBlob from \"@blob\";\nexport { contextToRequests, markdownToContext, DOC_MIME_TYPE };\nconst DOC_MIME_TYPE = \"application/vnd.google-apps.document\";\n// async function transformBlob(args: {\n//   contents: LLMContent[];\n//   transform: string;\n// }): Promise<{ contents: LLMContent[] }> {\n//   return args;\n// }\n/**\n * Removes surrounding backticks from each line if each\n * line is surrounded by backticks.\n * This is a bug in Google Drive markdown conversion.\n * Here's hoping it won't do too much damage.\n */\nfunction sanitizeBackticks(s) {\n    return s\n        .split(\"\\n\")\n        .map((line) => {\n        const trimmed = line.trim();\n        if (trimmed.length === 0)\n            return line;\n        if (trimmed.startsWith(\"`\") && trimmed.endsWith(\"`\")) {\n            return trimmed.slice(1, -1);\n        }\n        return line;\n    })\n        .join(\"\\n\");\n}\nconst BASE64_DATA_URL_REGEX = /^data:(.+?);base64,(.+)$/;\nfunction parseBase64DataUrl(url) {\n    const matchResult = url.match(BASE64_DATA_URL_REGEX);\n    if (!matchResult || matchResult.length !== 3) {\n        return null;\n    }\n    const [, mimeType, data] = matchResult;\n    return { inlineData: { mimeType, data } };\n}\nfunction markdownToContext(markdown) {\n    const tokens = marked.lexer(sanitizeBackticks(markdown));\n    const parts = mergeTextParts(tokens.flatMap((token) => {\n        if (token.type === \"paragraph\") {\n            return token.tokens.map((token) => {\n                if (token.type === \"image\") {\n                    const inlineData = parseBase64DataUrl(token.href);\n                    if (inlineData)\n                        return inlineData;\n                }\n                return { text: token.raw };\n            });\n        }\n        return { text: token.raw };\n    }));\n    return [{ parts }];\n}\nasync function contextToRequests(context, startIndex) {\n    const parts = context?.at(-1)?.parts;\n    if (!parts)\n        return [];\n    const result = [];\n    let index = startIndex;\n    for (const part of parts) {\n        if (\"text\" in part) {\n            const tokens = marked.lexer(toText(context));\n            const { lastIndex, requests } = tokensToRequests(tokens, index);\n            result.push(...requests);\n            index = lastIndex;\n        }\n        else if (\"inlineData\" in part) {\n            const contents = await transformBlob({\n                contents: [{ parts: [part] }],\n                transform: \"persistent-temporary\",\n            });\n            const storedPart = contents?.contents\n                ?.at(0)\n                ?.parts?.at(0);\n            if (storedPart) {\n                result.push({\n                    insertInlineImage: {\n                        uri: storedPart.storedData.handle,\n                        location: {\n                            index,\n                        },\n                    },\n                });\n            }\n        }\n    }\n    return result;\n}\n/**\n * Converts markdown tokens to Google Doc Request array for the\n * `batchUpdate` call.\n */\nfunction tokensToRequests(tokens, startIndex) {\n    let current = startIndex;\n    const requests = tokens.flatMap((token) => {\n        switch (token.type) {\n            case \"paragraph\":\n                return insertFormattedText(token, \"NORMAL_TEXT\");\n            case \"space\":\n                return insertSpace(token);\n            case \"code\":\n                return insertFormattedText(token, \"NORMAL_TEXT\");\n            case \"heading\":\n                return insertFormattedText(token, `HEADING_${token.depth}`);\n            case \"blockquote\":\n                return insertFormattedText(token, \"NORMAL_TEXT\");\n            case \"list\":\n                return insertList(token.items, token.ordered, 0);\n        }\n        return [];\n    });\n    return { lastIndex: current, requests };\n    function insertFormattedText(token, namedStyleType) {\n        const { requests, text: withoutBreak } = new TextStyles(current, token).parse();\n        const text = `${withoutBreak}\\n`;\n        if (namedStyleType) {\n            requests.unshift({\n                updateParagraphStyle: {\n                    range: range(text.length),\n                    paragraphStyle: { namedStyleType },\n                    fields: \"namedStyleType\",\n                },\n            });\n        }\n        requests.unshift({ insertText: { text, location: location() } });\n        current += text.length;\n        return requests;\n    }\n    function insertSpace(token) {\n        const text = token.raw.startsWith(\"\\n\") ? token.raw.slice(1) : token.raw;\n        const result = [\n            {\n                insertText: { text, location: location() },\n            },\n        ];\n        return advance(result, text.length);\n    }\n    function range(length) {\n        return { startIndex: current, endIndex: current + length };\n    }\n    function location() {\n        return { index: current };\n    }\n    function insertList(items, ordered, depth) {\n        const start = current;\n        // This is necessary to counteract a gnarly side-effect of creating a\n        // bullet list: the indent markers are being removed during that change,\n        // and change all of the ranges. So we have to make sure that the next\n        // request accounts for that.\n        let depthToRemove = 0;\n        const list = descendIntoList(items, ordered, depth).flat();\n        list.push({\n            createParagraphBullets: {\n                range: { startIndex: start, endIndex: current },\n                bulletPreset: \"BULLET_DISC_CIRCLE_SQUARE\",\n            },\n        });\n        current -= depthToRemove;\n        return list;\n        function descendIntoList(items, ordered, depth) {\n            const list = items.flatMap((item) => {\n                // For item, item.type === \"list_item\".\n                const indent = \"\\t\".repeat(depth);\n                depthToRemove += depth;\n                const result = [];\n                const maybeRichToken = \"tokens\" in item ? item : undefined;\n                const subList = maybeRichToken?.tokens.find((token) => token?.type === \"list\");\n                if (subList) {\n                    // assume that the first token is actually the text token.\n                    result.push(insertItemText(indent, maybeRichToken?.tokens.at(0)));\n                    result.push(...descendIntoList(subList.items, ordered, depth + 1));\n                }\n                else {\n                    result.push(insertItemText(indent, maybeRichToken?.tokens.at(0)));\n                }\n                return result;\n            });\n            return list;\n        }\n        function insertItemText(indent, token) {\n            const offset = current + indent.length;\n            const { requests, text: withoutIndent } = new TextStyles(offset, token).parse();\n            const text = `${indent}${withoutIndent}\\n`;\n            requests.unshift({\n                updateParagraphStyle: {\n                    range: range(text.length),\n                    paragraphStyle: { namedStyleType: \"NORMAL_TEXT\" },\n                    fields: \"namedStyleType\",\n                },\n            });\n            requests.unshift({ insertText: { text, location: location() } });\n            current += text.length;\n            return requests;\n        }\n    }\n    function advance(result, length) {\n        current += length;\n        return result;\n    }\n}\nclass TextStyles {\n    constructor(offset, token) {\n        _TextStyles_offset.set(this, void 0);\n        _TextStyles_tokens.set(this, []);\n        _TextStyles_styles.set(this, []);\n        __classPrivateFieldSet(this, _TextStyles_offset, offset, \"f\");\n        if (token && \"tokens\" in token) {\n            __classPrivateFieldSet(this, _TextStyles_tokens, token.tokens, \"f\");\n        }\n    }\n    range(startIndex, length) {\n        return { startIndex, endIndex: startIndex + length };\n    }\n    style(startIndex, length, textStyle) {\n        __classPrivateFieldGet(this, _TextStyles_styles, \"f\").push({\n            updateTextStyle: {\n                range: this.range(startIndex, length),\n                textStyle,\n                fields: Object.keys(textStyle).join(\",\"),\n            },\n        });\n    }\n    parse() {\n        let current = __classPrivateFieldGet(this, _TextStyles_offset, \"f\");\n        let text = \"\";\n        for (let token of __classPrivateFieldGet(this, _TextStyles_tokens, \"f\")) {\n            let tokenText = unescape(\"text\" in token ? token.text : \"\");\n            const length = tokenText.length;\n            text += tokenText;\n            switch (token.type) {\n                case \"strong\":\n                    this.style(current, length, { bold: true });\n                    break;\n                case \"em\":\n                    this.style(current, length, { italic: true });\n                    break;\n                case \"codespan\":\n                    this.style(current, length, {\n                        weightedFontFamily: {\n                            fontFamily: \"Fira Code\",\n                        },\n                    });\n                    break;\n                case \"del\":\n                    this.style(current, length, { strikethrough: true });\n                    break;\n                case \"link\":\n                    this.style(current, length, { link: { url: token.href } });\n                    break;\n                case \"escape\":\n                    console.log(\"ESCAPE\", token);\n                    break;\n            }\n            current += length;\n        }\n        return {\n            requests: __classPrivateFieldGet(this, _TextStyles_styles, \"f\"),\n            text,\n        };\n    }\n}\n_TextStyles_offset = new WeakMap(), _TextStyles_tokens = new WeakMap(), _TextStyles_styles = new WeakMap();\n",
       "metadata": {
         "title": "docs",
         "source": {
@@ -121,7 +121,7 @@
       }
     },
     "types": {
-      "code": "",
+      "code": "export {};\n",
       "metadata": {
         "title": "types",
         "source": {
@@ -133,7 +133,7 @@
       }
     },
     "connector-save": {
-      "code": "/**\n * @fileoverview Connector Save Export.\n */\nimport {} from \"@describe\";\nimport { toText, ok, err } from \"./a2/utils\";\nimport { contextToRequests, DOC_MIME_TYPE } from \"./docs\";\nimport { connect, query, create, getDoc, updateDoc } from \"./api\";\nexport { invoke as default, describe };\nasync function invoke({ method, id: connectorId, context, info, }) {\n    const mimeType = info?.configuration?.file?.mimeType;\n    const canSave = mimeType === DOC_MIME_TYPE || mimeType === undefined;\n    if (method === \"save\") {\n        if (!canSave) {\n            return err(`Unable to save files of type \"${mimeType}\"`);\n        }\n        const token = await connect({ title: \"Get Auth Token\" });\n        const gettingCollector = await getCollector(token, connectorId, \"Untitled Document\", info?.configuration?.file?.id);\n        if (!ok(gettingCollector))\n            return gettingCollector;\n        const { id, end } = gettingCollector;\n        const requests = await contextToRequests(context, end);\n        const updating = await updateDoc(token, id, { requests }, { title: \"Append to Google Doc\" });\n        if (!ok(updating))\n            return updating;\n        return { context: context || [] };\n    }\n    else if (method == \"canSave\") {\n        return { canSave };\n    }\n    return err(`Unknown method: \"${method}\"`);\n}\n/**\n * Gets or creates the Google Doc id that serves as the collector: the\n * doc to which context is appended.\n */\nasync function getCollector(token, connectorId, title, fileId) {\n    let id;\n    if (!fileId) {\n        const findFile = await query(token, `appProperties has { key = 'google-drive-connector' and value = '${connectorId}' } and trashed = false`, { title: \"Find the doc to append to\" });\n        if (!ok(findFile))\n            return findFile;\n        const file = findFile.files.at(0);\n        if (!file) {\n            const createdFile = await create(token, {\n                name: title,\n                mimeType: DOC_MIME_TYPE,\n                appProperties: {\n                    \"google-drive-connector\": connectorId,\n                },\n            }, { title: \"Create new doc to which to append\" });\n            if (!ok(createdFile))\n                return createdFile;\n            return {\n                id: createdFile.id,\n                end: 1,\n            };\n        }\n        id = file.id;\n    }\n    else {\n        id = fileId;\n    }\n    const gettingDoc = await getDoc(token, id, {\n        title: \"Get current doc contents\",\n    });\n    if (!ok(gettingDoc))\n        return gettingDoc;\n    const end = gettingDoc.body.content.reduce((acc, element) => Math.max(acc, element.endIndex || 0), 1) - 1;\n    return { id, end };\n}\nasync function describe() {\n    return {\n        title: \"Save To Google Drive\",\n        metadata: {\n            tags: [\"connector-save\"],\n        },\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                },\n            },\n        },\n    };\n}\n",
+      "code": "import { ok, err } from \"./a2/utils\";\nimport { contextToRequests, DOC_MIME_TYPE } from \"./docs\";\nimport { connect, query, create, getDoc, updateDoc } from \"./api\";\nexport { invoke as default, describe };\nasync function invoke({ method, id: connectorId, context, info, }) {\n    const mimeType = info?.configuration?.file?.mimeType;\n    const canSave = mimeType === DOC_MIME_TYPE || mimeType === undefined;\n    if (method === \"save\") {\n        if (!canSave) {\n            return err(`Unable to save files of type \"${mimeType}\"`);\n        }\n        const token = await connect({ title: \"Get Auth Token\" });\n        const gettingCollector = await getCollector(token, connectorId, \"Untitled Document\", info?.configuration?.file?.id);\n        if (!ok(gettingCollector))\n            return gettingCollector;\n        const { id, end } = gettingCollector;\n        const requests = await contextToRequests(context, end);\n        const updating = await updateDoc(token, id, { requests }, { title: \"Append to Google Doc\" });\n        if (!ok(updating))\n            return updating;\n        return { context: context || [] };\n    }\n    else if (method == \"canSave\") {\n        return { canSave };\n    }\n    return err(`Unknown method: \"${method}\"`);\n}\n/**\n * Gets or creates the Google Doc id that serves as the collector: the\n * doc to which context is appended.\n */\nasync function getCollector(token, connectorId, title, fileId) {\n    let id;\n    if (!fileId) {\n        const findFile = await query(token, `appProperties has { key = 'google-drive-connector' and value = '${connectorId}' } and trashed = false`, { title: \"Find the doc to append to\" });\n        if (!ok(findFile))\n            return findFile;\n        const file = findFile.files.at(0);\n        if (!file) {\n            const createdFile = await create(token, {\n                name: title,\n                mimeType: DOC_MIME_TYPE,\n                appProperties: {\n                    \"google-drive-connector\": connectorId,\n                },\n            }, { title: \"Create new doc to which to append\" });\n            if (!ok(createdFile))\n                return createdFile;\n            return {\n                id: createdFile.id,\n                end: 1,\n            };\n        }\n        id = file.id;\n    }\n    else {\n        id = fileId;\n    }\n    const gettingDoc = await getDoc(token, id, {\n        title: \"Get current doc contents\",\n    });\n    if (!ok(gettingDoc))\n        return gettingDoc;\n    const end = gettingDoc.body.content.reduce((acc, element) => Math.max(acc, element.endIndex || 0), 1) - 1;\n    return { id, end };\n}\nasync function describe() {\n    return {\n        title: \"Save To Google Drive\",\n        metadata: {\n            tags: [\"connector-save\"],\n        },\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                },\n            },\n        },\n    };\n}\n",
       "metadata": {
         "title": "connector-save",
         "source": {
@@ -145,7 +145,7 @@
       }
     },
     "connector-load": {
-      "code": "/**\n * @fileoverview Connector Load Export\n */\nimport {} from \"@describe\";\nimport { toText, ok, err, llm } from \"./a2/utils\";\nimport { contextToRequests, markdownToContext, DOC_MIME_TYPE } from \"./docs\";\nimport { connect, query, exp } from \"./api\";\nexport { invoke as default, describe };\nasync function invoke({ id, info: { configuration }, }) {\n    const token = await connect({ title: \"Getting auth token\" });\n    if (!ok(token))\n        return token;\n    const gettingDoc = await getCollector(token, id, configuration?.file);\n    if (!ok(gettingDoc))\n        return gettingDoc;\n    return { context: gettingDoc };\n}\n/**\n * Gets the Google Doc id that serves as the collector: the\n * doc to which context is appended.\n */\nasync function getCollector(token, connectorId, file) {\n    const { id: fileId, mimeType } = file || {};\n    let id;\n    if (!fileId) {\n        const findFile = await query(token, `appProperties has { key = 'google-drive-connector' and value = '${connectorId}' } and trashed = false`, { title: \"Find the doc to append to\" });\n        if (!ok(findFile))\n            return findFile;\n        const file = findFile.files.at(0);\n        if (!file) {\n            return [];\n        }\n        id = file.id;\n    }\n    else {\n        id = fileId;\n    }\n    const exporter = new Exporter(token, id, mimeType);\n    return exporter.export();\n}\nclass Exporter {\n    token;\n    id;\n    mimeType;\n    constructor(token, id, mimeType) {\n        this.token = token;\n        this.id = id;\n        this.mimeType = mimeType;\n    }\n    isDoc() {\n        return this.mimeType === DOC_MIME_TYPE;\n    }\n    async export() {\n        const { token, id } = this;\n        if (this.isDoc()) {\n            const gettingDoc = await exp(token, id, \"text/makdown\", {\n                title: \"Get current doc contents\",\n            });\n            if (!ok(gettingDoc))\n                return gettingDoc;\n            if (!(typeof gettingDoc === \"string\")) {\n                return err(`Invalid output from document export. Must be a string`);\n            }\n            return markdownToContext(gettingDoc);\n        }\n        else {\n            const exportingPdf = await exp(token, id, \"application/pdf\", {\n                title: \"Get PDF export of the file\",\n            });\n            if (!ok(exportingPdf))\n                return exportingPdf;\n            return [{ parts: [exportingPdf] }];\n        }\n    }\n}\nasync function describe() {\n    return {\n        metadata: {\n            tags: [\"connector-load\"],\n        },\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                },\n            },\n        },\n    };\n}\n",
+      "code": "import { ok, err } from \"./a2/utils\";\nimport { markdownToContext, DOC_MIME_TYPE } from \"./docs\";\nimport { connect, query, exp } from \"./api\";\nexport { invoke as default, describe };\nasync function invoke({ id, info: { configuration }, }) {\n    const token = await connect({ title: \"Getting auth token\" });\n    if (!ok(token))\n        return token;\n    const gettingDoc = await getCollector(token, id, configuration?.file);\n    if (!ok(gettingDoc))\n        return gettingDoc;\n    return { context: gettingDoc };\n}\n/**\n * Gets the Google Doc id that serves as the collector: the\n * doc to which context is appended.\n */\nasync function getCollector(token, connectorId, file) {\n    const { id: fileId, mimeType } = file || {};\n    let id;\n    if (!fileId) {\n        const findFile = await query(token, `appProperties has { key = 'google-drive-connector' and value = '${connectorId}' } and trashed = false`, { title: \"Find the doc to append to\" });\n        if (!ok(findFile))\n            return findFile;\n        const file = findFile.files.at(0);\n        if (!file) {\n            return [];\n        }\n        id = file.id;\n    }\n    else {\n        id = fileId;\n    }\n    const exporter = new Exporter(token, id, mimeType);\n    return exporter.export();\n}\nclass Exporter {\n    constructor(token, id, mimeType) {\n        this.token = token;\n        this.id = id;\n        this.mimeType = mimeType;\n    }\n    isDoc() {\n        return this.mimeType === DOC_MIME_TYPE;\n    }\n    async export() {\n        const { token, id } = this;\n        if (this.isDoc()) {\n            const gettingDoc = await exp(token, id, \"text/makdown\", {\n                title: \"Get current doc contents\",\n            });\n            if (!ok(gettingDoc))\n                return gettingDoc;\n            if (!(typeof gettingDoc === \"string\")) {\n                return err(`Invalid output from document export. Must be a string`);\n            }\n            return markdownToContext(gettingDoc);\n        }\n        else {\n            const exportingPdf = await exp(token, id, \"application/pdf\", {\n                title: \"Get PDF export of the file\",\n            });\n            if (!ok(exportingPdf))\n                return exportingPdf;\n            return [{ parts: [exportingPdf] }];\n        }\n    }\n}\nasync function describe() {\n    return {\n        metadata: {\n            tags: [\"connector-load\"],\n        },\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                },\n            },\n        },\n    };\n}\n",
       "metadata": {
         "title": "connector-load",
         "source": {
@@ -157,7 +157,7 @@
       }
     },
     "marked-types": {
-      "code": "/**\n * @fileoverview Marked Types\n */\n",
+      "code": "/**\n * @fileoverview Marked Types\n */\nexport {};\n",
       "metadata": {
         "title": "marked-types",
         "source": {
diff --git a/packages/a2/bgl/mcp.bgl.json b/packages/a2/bgl/mcp.bgl.json
index ed390871251..0bfc65aa99b 100644
--- a/packages/a2/bgl/mcp.bgl.json
+++ b/packages/a2/bgl/mcp.bgl.json
@@ -30,7 +30,12 @@
       }
     },
     "userModified": true,
-    "tags": ["connector", "connector-singleton", "experimental", "published"],
+    "tags": [
+      "connector",
+      "connector-singleton",
+      "experimental",
+      "published"
+    ],
     "comments": [
       {
         "id": "comment-c74afa15",
@@ -49,7 +54,7 @@
   },
   "modules": {
     "configurator": {
-      "code": "/**\n * @fileoverview Add a description for your module here.\n */\nimport { err, ok } from \"./a2/utils\";\nimport { createConfigurator } from \"./a2/connector-manager\";\nimport read from \"@read\";\nimport write from \"@write\";\nexport { invoke as default, describe };\nconst CONNECTOR_TITLE = \"MCP Server\";\nconst { invoke, describe } = createConfigurator({\n    title: CONNECTOR_TITLE,\n    initialize: async () => {\n        return { title: CONNECTOR_TITLE, configuration: {} };\n    },\n    read: async ({ id, configuration }) => {\n        return {\n            schema: {\n                type: \"object\",\n                properties: {\n                    endpoint: {\n                        type: \"string\",\n                        title: \"URL\",\n                        description: \"The URL of MCP Server SSE endpoint\",\n                    },\n                },\n            },\n            values: configuration,\n        };\n    },\n});\n",
+      "code": "/**\n * @fileoverview Add a description for your module here.\n */\nimport { createConfigurator } from \"./a2/connector-manager\";\nexport { invoke as default, describe };\nconst CONNECTOR_TITLE = \"MCP Server\";\nconst { invoke, describe } = createConfigurator({\n    title: CONNECTOR_TITLE,\n    initialize: async () => {\n        return { title: CONNECTOR_TITLE, configuration: {} };\n    },\n    read: async ({ id, configuration }) => {\n        return {\n            schema: {\n                type: \"object\",\n                properties: {\n                    endpoint: {\n                        type: \"string\",\n                        title: \"URL\",\n                        description: \"The URL of MCP Server SSE endpoint\",\n                    },\n                },\n            },\n            values: configuration,\n        };\n    },\n});\n",
       "metadata": {
         "title": "configurator",
         "source": {
@@ -61,7 +66,7 @@
       }
     },
     "mcp-client": {
-      "code": "/**\n * @fileoverview Breadboard MCP Client\n */\nimport fetch from \"@fetch\";\nimport read from \"@read\";\nimport write from \"@write\";\nimport query from \"@query\";\nimport { ok, err } from \"./a2/utils\";\nexport { McpClient };\nfunction json(result) {\n    if (!ok(result))\n        return result;\n    const j = result.data?.at(-1)?.parts.at(0)?.json;\n    if (!j) {\n        return err(`Invalid result structure`);\n    }\n    return j;\n}\nfunction sse(result) {\n    const e = json(result);\n    if (!ok(e))\n        return e;\n    return e.data;\n}\nfunction rpc(result) {\n    const e = sse(result);\n    if (!ok(e))\n        return e;\n    return e.result;\n}\nclass McpClient {\n    connectorId;\n    url;\n    #id = 0;\n    #messageEndpoint = null;\n    constructor(connectorId, url) {\n        this.connectorId = connectorId;\n        this.url = url;\n    }\n    #path() {\n        return `/session/mcp/${this.connectorId}/stream`;\n    }\n    #sessionIdPath() {\n        return `/session/mcp/${this.connectorId}/id`;\n    }\n    #newId() {\n        return ++this.#id;\n    }\n    async connect() {\n        if (this.#messageEndpoint) {\n            return err(`Already connected to \"${this.#messageEndpoint}\"`);\n        }\n        const file = this.#path();\n        const url = this.url;\n        const savedSession = json(await read({ path: this.#sessionIdPath() }));\n        if (ok(savedSession)) {\n            console.log(\"EXISTING SESSION\", savedSession);\n            this.#messageEndpoint = savedSession.endpoint;\n            return;\n        }\n        // Establish connection.\n        const connecting = await fetch({ url, file, stream: \"sse\" });\n        if (!ok(connecting))\n            return connecting;\n        // This is the connection path.\n        const path = connecting.response;\n        // Read the endpoint event\n        const connection = sse(await read({ path }));\n        if (!ok(connection))\n            return connection;\n        // do some crude URL munging.\n        const hostname = url.split(\"/\").slice(0, -1).join(\"/\");\n        const messageEndpoint = `${hostname}${connection}`;\n        const id = this.#newId();\n        // send initialize request\n        const initializing = await fetch({\n            url: messageEndpoint,\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({\n                jsonrpc: \"2.0\",\n                id,\n                method: \"initialize\",\n                params: {\n                    protocolVersion: \"2024-11-05\",\n                    clientInfo: {\n                        name: \"Breadboard\",\n                        version: \"1.0.0\",\n                    },\n                    capabilities: {\n                        tools: {},\n                    },\n                },\n            }),\n        });\n        if (!ok(initializing))\n            return initializing;\n        const initializeResponse = rpc(await read({ path }));\n        if (!ok(initializeResponse))\n            return initializeResponse;\n        const confirmInitialization = await fetch({\n            url: messageEndpoint,\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({\n                jsonrpc: \"2.0\",\n                method: \"notifications/initialized\",\n            }),\n        });\n        if (!ok(confirmInitialization))\n            return confirmInitialization;\n        this.#messageEndpoint = messageEndpoint;\n        // Save message endpoint\n        const savingMessageEndpoint = await write({\n            path: this.#sessionIdPath(),\n            data: [\n                {\n                    parts: [{ json: { endpoint: messageEndpoint } }],\n                },\n            ],\n        });\n        if (!ok(savingMessageEndpoint))\n            return savingMessageEndpoint;\n    }\n    async listTools() {\n        if (!this.#messageEndpoint) {\n            return err(`The client wasn't initialized. Call \"connect\" first.`);\n        }\n        const url = this.#messageEndpoint;\n        const id = this.#newId();\n        const path = this.#path();\n        // get list of tools\n        const askToListTools = await fetch({\n            url,\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({\n                jsonrpc: \"2.0\",\n                id,\n                method: \"tools/list\",\n            }),\n        });\n        if (!ok(askToListTools))\n            return askToListTools;\n        const toolList = rpc(await read({ path }));\n        if (!ok(toolList))\n            return toolList;\n        return toolList.tools;\n    }\n    async callTool(name, args) {\n        if (!this.#messageEndpoint) {\n            return err(`The client wasn't initialized. Call \"connect\" first.`);\n        }\n        const url = this.#messageEndpoint;\n        const id = this.#newId();\n        const path = this.#path();\n        // Call tool\n        const askToCallTool = await fetch({\n            url,\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({\n                jsonrpc: \"2.0\",\n                id,\n                method: \"tools/call\",\n                params: { name, arguments: args },\n            }),\n        });\n        const readCallToolResults = rpc(await read({ path }));\n        if (!ok(readCallToolResults))\n            return readCallToolResults;\n        return readCallToolResults.content;\n    }\n    async disconnect() {\n        const path = this.#path();\n        // Close the stream.\n        const deleting = await write({ path, delete: true });\n        // Delete the saved session\n        const deletingSession = await write({\n            path: this.#sessionIdPath(),\n            delete: true,\n        });\n        if (!ok(deleting))\n            return deleting;\n        if (!ok(deletingSession))\n            return deletingSession;\n    }\n}\n",
+      "code": "/**\n * @fileoverview Breadboard MCP Client\n */\nvar __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n    return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n};\nvar __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {\n    if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\n    if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\n    if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\n    return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\n};\nvar _McpClient_instances, _McpClient_id, _McpClient_messageEndpoint, _McpClient_path, _McpClient_sessionIdPath, _McpClient_newId;\nimport fetch from \"@fetch\";\nimport read from \"@read\";\nimport write from \"@write\";\nimport { ok, err } from \"./a2/utils\";\nexport { McpClient };\nfunction json(result) {\n    if (!ok(result))\n        return result;\n    const j = result.data?.at(-1)?.parts.at(0)?.json;\n    if (!j) {\n        return err(`Invalid result structure`);\n    }\n    return j;\n}\nfunction sse(result) {\n    const e = json(result);\n    if (!ok(e))\n        return e;\n    return e.data;\n}\nfunction rpc(result) {\n    const e = sse(result);\n    if (!ok(e))\n        return e;\n    return e.result;\n}\nclass McpClient {\n    constructor(connectorId, url) {\n        _McpClient_instances.add(this);\n        this.connectorId = connectorId;\n        this.url = url;\n        _McpClient_id.set(this, 0);\n        _McpClient_messageEndpoint.set(this, null);\n    }\n    async connect() {\n        if (__classPrivateFieldGet(this, _McpClient_messageEndpoint, \"f\")) {\n            return err(`Already connected to \"${__classPrivateFieldGet(this, _McpClient_messageEndpoint, \"f\")}\"`);\n        }\n        const file = __classPrivateFieldGet(this, _McpClient_instances, \"m\", _McpClient_path).call(this);\n        const url = this.url;\n        const savedSession = json(await read({ path: __classPrivateFieldGet(this, _McpClient_instances, \"m\", _McpClient_sessionIdPath).call(this) }));\n        if (ok(savedSession)) {\n            console.log(\"EXISTING SESSION\", savedSession);\n            __classPrivateFieldSet(this, _McpClient_messageEndpoint, savedSession.endpoint, \"f\");\n            return;\n        }\n        // Establish connection.\n        const connecting = await fetch({ url, file, stream: \"sse\" });\n        if (!ok(connecting))\n            return connecting;\n        // This is the connection path.\n        const path = connecting.response;\n        // Read the endpoint event\n        const connection = sse(await read({ path }));\n        if (!ok(connection))\n            return connection;\n        // do some crude URL munging.\n        const hostname = url.split(\"/\").slice(0, -1).join(\"/\");\n        const messageEndpoint = `${hostname}${connection}`;\n        const id = __classPrivateFieldGet(this, _McpClient_instances, \"m\", _McpClient_newId).call(this);\n        // send initialize request\n        const initializing = await fetch({\n            url: messageEndpoint,\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({\n                jsonrpc: \"2.0\",\n                id,\n                method: \"initialize\",\n                params: {\n                    protocolVersion: \"2024-11-05\",\n                    clientInfo: {\n                        name: \"Breadboard\",\n                        version: \"1.0.0\",\n                    },\n                    capabilities: {\n                        tools: {},\n                    },\n                },\n            }),\n        });\n        if (!ok(initializing))\n            return initializing;\n        const initializeResponse = rpc(await read({ path }));\n        if (!ok(initializeResponse))\n            return initializeResponse;\n        const confirmInitialization = await fetch({\n            url: messageEndpoint,\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({\n                jsonrpc: \"2.0\",\n                method: \"notifications/initialized\",\n            }),\n        });\n        if (!ok(confirmInitialization))\n            return confirmInitialization;\n        __classPrivateFieldSet(this, _McpClient_messageEndpoint, messageEndpoint, \"f\");\n        // Save message endpoint\n        const savingMessageEndpoint = await write({\n            path: __classPrivateFieldGet(this, _McpClient_instances, \"m\", _McpClient_sessionIdPath).call(this),\n            data: [\n                {\n                    parts: [{ json: { endpoint: messageEndpoint } }],\n                },\n            ],\n        });\n        if (!ok(savingMessageEndpoint))\n            return savingMessageEndpoint;\n    }\n    async listTools() {\n        if (!__classPrivateFieldGet(this, _McpClient_messageEndpoint, \"f\")) {\n            return err(`The client wasn't initialized. Call \"connect\" first.`);\n        }\n        const url = __classPrivateFieldGet(this, _McpClient_messageEndpoint, \"f\");\n        const id = __classPrivateFieldGet(this, _McpClient_instances, \"m\", _McpClient_newId).call(this);\n        const path = __classPrivateFieldGet(this, _McpClient_instances, \"m\", _McpClient_path).call(this);\n        // get list of tools\n        const askToListTools = await fetch({\n            url,\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({\n                jsonrpc: \"2.0\",\n                id,\n                method: \"tools/list\",\n            }),\n        });\n        if (!ok(askToListTools))\n            return askToListTools;\n        const toolList = rpc(await read({ path }));\n        if (!ok(toolList))\n            return toolList;\n        return toolList.tools;\n    }\n    async callTool(name, args) {\n        if (!__classPrivateFieldGet(this, _McpClient_messageEndpoint, \"f\")) {\n            return err(`The client wasn't initialized. Call \"connect\" first.`);\n        }\n        const url = __classPrivateFieldGet(this, _McpClient_messageEndpoint, \"f\");\n        const id = __classPrivateFieldGet(this, _McpClient_instances, \"m\", _McpClient_newId).call(this);\n        const path = __classPrivateFieldGet(this, _McpClient_instances, \"m\", _McpClient_path).call(this);\n        // Call tool\n        const askToCallTool = await fetch({\n            url,\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({\n                jsonrpc: \"2.0\",\n                id,\n                method: \"tools/call\",\n                params: { name, arguments: args },\n            }),\n        });\n        const readCallToolResults = rpc(await read({ path }));\n        if (!ok(readCallToolResults))\n            return readCallToolResults;\n        return readCallToolResults.content;\n    }\n    async disconnect() {\n        const path = __classPrivateFieldGet(this, _McpClient_instances, \"m\", _McpClient_path).call(this);\n        // Close the stream.\n        const deleting = await write({ path, delete: true });\n        // Delete the saved session\n        const deletingSession = await write({\n            path: __classPrivateFieldGet(this, _McpClient_instances, \"m\", _McpClient_sessionIdPath).call(this),\n            delete: true,\n        });\n        if (!ok(deleting))\n            return deleting;\n        if (!ok(deletingSession))\n            return deletingSession;\n    }\n}\n_McpClient_id = new WeakMap(), _McpClient_messageEndpoint = new WeakMap(), _McpClient_instances = new WeakSet(), _McpClient_path = function _McpClient_path() {\n    return `/session/mcp/${this.connectorId}/stream`;\n}, _McpClient_sessionIdPath = function _McpClient_sessionIdPath() {\n    return `/session/mcp/${this.connectorId}/id`;\n}, _McpClient_newId = function _McpClient_newId() {\n    var _a;\n    return __classPrivateFieldSet(this, _McpClient_id, (_a = __classPrivateFieldGet(this, _McpClient_id, \"f\"), ++_a), \"f\");\n};\n",
       "metadata": {
         "title": "mcp-client",
         "source": {
@@ -73,7 +78,7 @@
       }
     },
     "connector-tools": {
-      "code": "/**\n * @fileoverview The tools export for the connector.\n */\nimport { err, ok } from \"./a2/utils\";\nimport { McpClient } from \"./mcp-client\";\nimport { createTools } from \"./a2/connector-manager\";\nexport { invoke as default, describe };\nasync function getClient(id, info) {\n    // for now, wrap it to point at MCP proxy\n    const url = `http://127.0.0.1:6277/sse?transportType=sse&url=${encodeURIComponent(info.configuration.endpoint)}`;\n    const client = new McpClient(id, url);\n    const connecting = await client.connect();\n    if (!ok(connecting))\n        return connecting;\n    return client;\n}\nconst { invoke, describe } = createTools({\n    title: \"MCP Server\",\n    list: async (id, info) => {\n        const client = await getClient(id, info);\n        if (!ok(client))\n            return client;\n        const listing = await client.listTools();\n        if (!ok(listing))\n            return listing;\n        // Transform to the ToolManager format.\n        const list = listing.map((item) => {\n            return {\n                url: info.url,\n                description: { ...item, title: item.name },\n                passContext: false,\n            };\n        });\n        return { list };\n    },\n    invoke: async (id, info, name, args) => {\n        const client = await getClient(id, info);\n        if (!ok(client))\n            return client;\n        const connecting = await client.connect();\n        if (!ok(connecting))\n            return connecting;\n        const invoking = await client.callTool(name, args);\n        if (!ok(invoking))\n            return invoking;\n        return { result: JSON.stringify(invoking) };\n    },\n});\n",
+      "code": "/**\n * @fileoverview The tools export for the connector.\n */\nimport { ok } from \"./a2/utils\";\nimport { McpClient } from \"./mcp-client\";\nimport { createTools } from \"./a2/connector-manager\";\nexport { invoke as default, describe };\nasync function getClient(id, info) {\n    // for now, wrap it to point at MCP proxy\n    const url = `http://127.0.0.1:6277/sse?transportType=sse&url=${encodeURIComponent(info.configuration.endpoint)}`;\n    const client = new McpClient(id, url);\n    const connecting = await client.connect();\n    if (!ok(connecting))\n        return connecting;\n    return client;\n}\nconst { invoke, describe } = createTools({\n    title: \"MCP Server\",\n    list: async (id, info) => {\n        const client = await getClient(id, info);\n        if (!ok(client))\n            return client;\n        const listing = await client.listTools();\n        if (!ok(listing))\n            return listing;\n        // Transform to the ToolManager format.\n        const list = listing.map((item) => {\n            return {\n                url: info.url,\n                description: { ...item, title: item.name },\n                passContext: false,\n            };\n        });\n        return { list };\n    },\n    invoke: async (id, info, name, args) => {\n        const client = await getClient(id, info);\n        if (!ok(client))\n            return client;\n        const connecting = await client.connect();\n        if (!ok(connecting))\n            return connecting;\n        const invoking = await client.callTool(name, args);\n        if (!ok(invoking))\n            return invoking;\n        return { result: JSON.stringify(invoking) };\n    },\n});\n",
       "metadata": {
         "title": "connector-tools",
         "source": {
@@ -90,5 +95,8 @@
       "url": "./a2.bgl.json"
     }
   },
-  "exports": ["#module:configurator", "#module:connector-tools"]
-}
+  "exports": [
+    "#module:configurator",
+    "#module:connector-tools"
+  ]
+}
\ No newline at end of file
diff --git a/packages/a2/bgl/notebooklm-connector.bgl.json b/packages/a2/bgl/notebooklm-connector.bgl.json
index 10117ea2205..750c5bbdcf2 100644
--- a/packages/a2/bgl/notebooklm-connector.bgl.json
+++ b/packages/a2/bgl/notebooklm-connector.bgl.json
@@ -30,7 +30,11 @@
       }
     },
     "userModified": true,
-    "tags": ["connector", "published", "experimental"],
+    "tags": [
+      "connector",
+      "published",
+      "experimental"
+    ],
     "comments": [
       {
         "id": "comment-c74afa15",
@@ -49,7 +53,7 @@
   },
   "modules": {
     "configurator": {
-      "code": "/**\n * @fileoverview Add a description for your module here.\n */\nimport { err, ok } from \"./a2/utils\";\nimport { createConfigurator } from \"./a2/connector-manager\";\nimport { executeTool } from \"./a2/step-executor\";\nimport read from \"@read\";\nimport write from \"@write\";\nexport { invoke as default, describe };\nconst { invoke, describe } = createConfigurator({\n    title: \"NotebookLM\",\n    initialize: async () => {\n        return {\n            title: \"Untitled NotebookLM\",\n            configuration: {},\n        };\n    },\n    read: async ({ id, configuration }) => {\n        const readingNotebooks = await executeTool(\"notebooklm_list_projects\", {});\n        if (!ok(readingNotebooks))\n            return readingNotebooks;\n        console.log(\"READING NOTEBOOKS\", readingNotebooks);\n        return {\n            schema: {\n                type: \"object\",\n                properties: {\n                    notebookId: {\n                        type: \"string\",\n                        title: \"Notebook\",\n                        description: \"The notebook to query\",\n                    },\n                },\n            },\n            values: configuration,\n        };\n    },\n});\n",
+      "code": "/**\n * @fileoverview Add a description for your module here.\n */\nimport { ok } from \"./a2/utils\";\nimport { createConfigurator } from \"./a2/connector-manager\";\nimport { executeTool } from \"./a2/step-executor\";\nexport { invoke as default, describe };\nconst { invoke, describe } = createConfigurator({\n    title: \"NotebookLM\",\n    initialize: async () => {\n        return {\n            title: \"Untitled NotebookLM\",\n            configuration: {},\n        };\n    },\n    read: async ({ id, configuration }) => {\n        const readingNotebooks = await executeTool(\"notebooklm_list_projects\", {});\n        if (!ok(readingNotebooks))\n            return readingNotebooks;\n        console.log(\"READING NOTEBOOKS\", readingNotebooks);\n        return {\n            schema: {\n                type: \"object\",\n                properties: {\n                    notebookId: {\n                        type: \"string\",\n                        title: \"Notebook\",\n                        description: \"The notebook to query\",\n                    },\n                },\n            },\n            values: configuration,\n        };\n    },\n});\n",
       "metadata": {
         "title": "configurator",
         "source": {
@@ -73,7 +77,7 @@
       }
     },
     "types": {
-      "code": "/**\n * @fileoverview Commmon types for the connector.\n */\n",
+      "code": "/**\n * @fileoverview Commmon types for the connector.\n */\nexport {};\n",
       "metadata": {
         "title": "types",
         "source": {
@@ -90,5 +94,8 @@
       "url": "./a2.bgl.json"
     }
   },
-  "exports": ["#module:configurator", "#module:connector-tools"]
-}
+  "exports": [
+    "#module:configurator",
+    "#module:connector-tools"
+  ]
+}
\ No newline at end of file
diff --git a/packages/a2/bgl/tools.bgl.json b/packages/a2/bgl/tools.bgl.json
index 072cbc74fbd..62c4d5c2b32 100644
--- a/packages/a2/bgl/tools.bgl.json
+++ b/packages/a2/bgl/tools.bgl.json
@@ -55,7 +55,7 @@
   },
   "modules": {
     "search-web": {
-      "code": "/**\n * @fileoverview Given a query, searches the Web with Google Search.\n */\nimport gemini, {} from \"./a2/gemini\";\nimport { Template } from \"./a2/template\";\nimport { ToolManager } from \"./a2/tool-manager\";\nimport { GeminiPrompt } from \"./a2/gemini-prompt\";\nimport { ok, err, toText, toTextConcat, toLLMContent, addUserTurn, defaultLLMContent, } from \"./a2/utils\";\nimport { ListExpander } from \"./a2/lists\";\nimport toolSearchWeb, { describe as toolSearchWebDescribe, } from \"./tool-search-web\";\nimport fetch from \"@fetch\";\nexport { invoke as default, describe };\nasync function resolveInput(inputContent) {\n    const template = new Template(inputContent);\n    const substituting = await template.substitute({}, async () => \"\");\n    if (!ok(substituting)) {\n        return toLLMContent(substituting.$error);\n    }\n    return substituting;\n}\nfunction extractQuery(maybeMarkdownListItem) {\n    if (maybeMarkdownListItem.startsWith(\"* \")) {\n        return maybeMarkdownListItem.replace(\"* \", \"\");\n    }\n    return maybeMarkdownListItem;\n}\nasync function invoke(inputs) {\n    let query;\n    let mode;\n    if (\"context\" in inputs) {\n        mode = \"step\";\n        const last = inputs.context?.at(-1);\n        if (inputs.context) {\n            query = inputs.context;\n        }\n        else {\n            return err(\"Please provide a URL\");\n        }\n    }\n    else if (\"p-query\" in inputs) {\n        const queryContent = await resolveInput(inputs[\"p-query\"]);\n        if (!ok(queryContent)) {\n            return queryContent;\n        }\n        query = [queryContent];\n        mode = \"step\";\n    }\n    else {\n        query = [toLLMContent(inputs.query)];\n        mode = \"tool\";\n    }\n    const searchResults = await new ListExpander(toLLMContent(defaultLLMContent()), query).map(async (_, itemContext) => {\n        let queryString = extractQuery(toText(itemContext));\n        queryString = (queryString || \"\").trim();\n        if (!queryString) {\n            return err(\"Please provide a query\");\n        }\n        console.log(\"Query: \", queryString);\n        const getting = await toolSearchWeb({ query: queryString });\n        if (!ok(getting)) {\n            return toLLMContent(getting.$error);\n        }\n        return toLLMContent(getting.results);\n    });\n    if (!ok(searchResults)) {\n        return searchResults;\n    }\n    if (mode === \"step\") {\n        return {\n            context: searchResults,\n        };\n    }\n    return { results: toTextConcat(searchResults) };\n}\nasync function describe({ asType, ...inputs }) {\n    const isTool = inputs && Object.keys(inputs).length === 1;\n    if (isTool) {\n        return toolSearchWebDescribe();\n    }\n    const hasWires = \"context\" in (inputs.inputSchema.properties || {});\n    const query = hasWires\n        ? {}\n        : {\n            \"p-query\": {\n                type: \"object\",\n                title: \"Search query\",\n                description: \"Please provide a search query\",\n                behavior: [\n                    \"llm-content\",\n                    \"config\",\n                    \"hint-preview\",\n                    \"hint-single-line\",\n                ],\n                default: defaultLLMContent(),\n            },\n        };\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: {\n                        type: \"object\",\n                        behavior: [\"llm-content\"],\n                    },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                ...query,\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"main-port\"],\n                },\n            },\n        },\n        metadata: {\n            icon: \"search\",\n            tags: [\"quick-access\", \"tool\", \"component\"],\n            order: 1,\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Given a query, searches the Web with Google Search.\n */\nimport { Template } from \"./a2/template\";\nimport { ok, err, toText, toTextConcat, toLLMContent, defaultLLMContent, } from \"./a2/utils\";\nimport { ListExpander } from \"./a2/lists\";\nimport toolSearchWeb, { describe as toolSearchWebDescribe, } from \"./tool-search-web\";\nexport { invoke as default, describe };\nasync function resolveInput(inputContent) {\n    const template = new Template(inputContent);\n    const substituting = await template.substitute({}, async () => \"\");\n    if (!ok(substituting)) {\n        return toLLMContent(substituting.$error);\n    }\n    return substituting;\n}\nfunction extractQuery(maybeMarkdownListItem) {\n    if (maybeMarkdownListItem.startsWith(\"* \")) {\n        return maybeMarkdownListItem.replace(\"* \", \"\");\n    }\n    return maybeMarkdownListItem;\n}\nasync function invoke(inputs) {\n    let query;\n    let mode;\n    if (\"context\" in inputs) {\n        mode = \"step\";\n        const last = inputs.context?.at(-1);\n        if (inputs.context) {\n            query = inputs.context;\n        }\n        else {\n            return err(\"Please provide a URL\");\n        }\n    }\n    else if (\"p-query\" in inputs) {\n        const queryContent = await resolveInput(inputs[\"p-query\"]);\n        if (!ok(queryContent)) {\n            return queryContent;\n        }\n        query = [queryContent];\n        mode = \"step\";\n    }\n    else {\n        query = [toLLMContent(inputs.query)];\n        mode = \"tool\";\n    }\n    const searchResults = await new ListExpander(toLLMContent(defaultLLMContent()), query).map(async (_, itemContext) => {\n        let queryString = extractQuery(toText(itemContext));\n        queryString = (queryString || \"\").trim();\n        if (!queryString) {\n            return err(\"Please provide a query\");\n        }\n        console.log(\"Query: \", queryString);\n        const getting = await toolSearchWeb({ query: queryString });\n        if (!ok(getting)) {\n            return toLLMContent(getting.$error);\n        }\n        return toLLMContent(getting.results);\n    });\n    if (!ok(searchResults)) {\n        return searchResults;\n    }\n    if (mode === \"step\") {\n        return {\n            context: searchResults,\n        };\n    }\n    return { results: toTextConcat(searchResults) };\n}\nasync function describe({ asType, ...inputs }) {\n    const isTool = inputs && Object.keys(inputs).length === 1;\n    if (isTool) {\n        return toolSearchWebDescribe();\n    }\n    const hasWires = \"context\" in (inputs.inputSchema.properties || {});\n    const query = hasWires\n        ? {}\n        : {\n            \"p-query\": {\n                type: \"object\",\n                title: \"Search query\",\n                description: \"Please provide a search query\",\n                behavior: [\n                    \"llm-content\",\n                    \"config\",\n                    \"hint-preview\",\n                    \"hint-single-line\",\n                ],\n                default: defaultLLMContent(),\n            },\n        };\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: {\n                        type: \"object\",\n                        behavior: [\"llm-content\"],\n                    },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                ...query,\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"main-port\"],\n                },\n            },\n        },\n        metadata: {\n            icon: \"search\",\n            tags: [\"quick-access\", \"tool\", \"component\"],\n            order: 1,\n        },\n    };\n}\n",
       "metadata": {
         "title": "Search Web",
         "source": {
@@ -67,11 +67,11 @@
       }
     },
     "search-maps": {
-      "code": "/**\n * @fileoverview Search for location information.\n */\nimport toolSearchMaps, { describe as toolSearchMapsDescribe, } from \"./tool-search-maps\";\nimport { Template } from \"./a2/template\";\nimport { err, ok, toText, toLLMContent, defaultLLMContent } from \"./a2/utils\";\nexport { invoke as default, describe };\nasync function resolveInput(inputContent) {\n    const template = new Template(inputContent);\n    const substituting = await template.substitute({}, async () => \"\");\n    if (!ok(substituting)) {\n        return substituting.$error;\n    }\n    return toText(substituting);\n}\nasync function invoke(inputs) {\n    console.log(\"MAPS INPUTS\", inputs);\n    let query;\n    let mode;\n    if (\"context\" in inputs) {\n        mode = \"step\";\n        const last = inputs.context?.at(-1);\n        if (last) {\n            query = toText(last);\n        }\n        else {\n            return err(\"Please provide a query\");\n        }\n    }\n    else if (\"p-query\" in inputs) {\n        query = await resolveInput(inputs[\"p-query\"]);\n        mode = \"step\";\n    }\n    else {\n        query = inputs.query;\n        mode = \"tool\";\n    }\n    query = (query || \"\").trim();\n    if (!query) {\n        return err(\"Please provide a query\");\n    }\n    console.log(\"Query: \" + query);\n    const searchMapsResults = await toolSearchMaps({ query });\n    if (!ok(searchMapsResults)) {\n        return searchMapsResults;\n    }\n    const results = searchMapsResults.results;\n    if (mode === \"step\") {\n        return {\n            context: [toLLMContent(results)],\n        };\n    }\n    return { results };\n}\nasync function describe({ asType, ...inputs }) {\n    const isTool = inputs && Object.keys(inputs).length === 1;\n    if (isTool) {\n        return toolSearchMapsDescribe();\n    }\n    const hasWires = \"context\" in (inputs.inputSchema.properties || {});\n    const query = hasWires\n        ? {}\n        : {\n            \"p-query\": {\n                type: \"object\",\n                title: \"Search query\",\n                description: \"Please provide a search query\",\n                behavior: [\n                    \"llm-content\",\n                    \"config\",\n                    \"hint-preview\",\n                    \"hint-single-line\",\n                ],\n                default: defaultLLMContent(),\n            },\n        };\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                ...query,\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"main-port\"],\n                },\n            },\n        },\n        metadata: {\n            icon: \"map-search\",\n            tags: [\"quick-access\", \"tool\", \"component\"],\n            order: 2,\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Add a description for your module here.\n */\nimport toolSearchMaps, { describe as toolSearchMapsDescribe, } from \"./tool-search-maps\";\nimport { Template } from \"./a2/template\";\nimport { err, ok, toText, toLLMContent, defaultLLMContent } from \"./a2/utils\";\nexport { invoke as default, describe };\nasync function resolveInput(inputContent) {\n    const template = new Template(inputContent);\n    const substituting = await template.substitute({}, async () => \"\");\n    if (!ok(substituting)) {\n        return substituting.$error;\n    }\n    return toText(substituting);\n}\nasync function invoke(inputs) {\n    console.log(\"MAPS INPUTS\", inputs);\n    let query;\n    let mode;\n    if (\"context\" in inputs) {\n        mode = \"step\";\n        const last = inputs.context?.at(-1);\n        if (last) {\n            query = toText(last);\n        }\n        else {\n            return err(\"Please provide a query\");\n        }\n    }\n    else if (\"p-query\" in inputs) {\n        query = await resolveInput(inputs[\"p-query\"]);\n        mode = \"step\";\n    }\n    else {\n        query = inputs.query;\n        mode = \"tool\";\n    }\n    query = (query || \"\").trim();\n    if (!query) {\n        return err(\"Please provide a query\");\n    }\n    console.log(\"Query: \" + query);\n    const searchMapsResults = await toolSearchMaps({ query });\n    if (!ok(searchMapsResults)) {\n        return searchMapsResults;\n    }\n    const results = searchMapsResults.results;\n    if (mode === \"step\") {\n        return {\n            context: [toLLMContent(results)],\n        };\n    }\n    return { results };\n}\nasync function describe({ asType, ...inputs }) {\n    const isTool = inputs && Object.keys(inputs).length === 1;\n    if (isTool) {\n        return toolSearchMapsDescribe();\n    }\n    const hasWires = \"context\" in (inputs.inputSchema.properties || {});\n    const query = hasWires\n        ? {}\n        : {\n            \"p-query\": {\n                type: \"object\",\n                title: \"Search query\",\n                description: \"Please provide a search query\",\n                behavior: [\n                    \"llm-content\",\n                    \"config\",\n                    \"hint-preview\",\n                    \"hint-single-line\",\n                ],\n                default: defaultLLMContent(),\n            },\n        };\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                ...query,\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"main-port\"],\n                },\n            },\n        },\n        metadata: {\n            icon: \"map-search\",\n            tags: [\"quick-access\", \"tool\", \"component\"],\n            order: 2,\n        },\n    };\n}\n",
       "metadata": {
         "title": "Search Maps",
         "source": {
-          "code": "/**\n * @fileoverview Search for location information.\n */\n\nimport toolSearchMaps, {\n  type SearchMapsOutputs,\n  describe as toolSearchMapsDescribe,\n} from \"./tool-search-maps\";\nimport { Template } from \"./a2/template\";\nimport { err, ok, toText, toLLMContent, defaultLLMContent } from \"./a2/utils\";\nexport { invoke as default, describe };\n\ntype Inputs =\n  | {\n      context?: LLMContent[];\n      \"p-query\": LLMContent;\n    }\n  | {\n      query: string;\n    };\n\ntype Outputs =\n  | {\n      context: LLMContent[];\n    }\n  | {\n      results: string;\n    };\n\nasync function resolveInput(inputContent: LLMContent): Promise<string> {\n  const template = new Template(inputContent);\n  const substituting = await template.substitute({}, async () => \"\");\n  if (!ok(substituting)) {\n    return substituting.$error;\n  }\n  return toText(substituting);\n}\n\nasync function invoke(inputs: Inputs): Promise<Outcome<Outputs>> {\n  console.log(\"MAPS INPUTS\", inputs);\n  let query: string;\n  let mode: \"step\" | \"tool\";\n  if (\"context\" in inputs) {\n    mode = \"step\";\n    const last = inputs.context?.at(-1);\n    if (last) {\n      query = toText(last);\n    } else {\n      return err(\"Please provide a query\");\n    }\n  } else if (\"p-query\" in inputs) {\n    query = await resolveInput(inputs[\"p-query\"]);\n    mode = \"step\";\n  } else {\n    query = inputs.query;\n    mode = \"tool\";\n  }\n  query = (query || \"\").trim();\n  if (!query) {\n    return err(\"Please provide a query\");\n  }\n\n  console.log(\"Query: \" + query);\n  const searchMapsResults = await toolSearchMaps({ query });\n  if (!ok(searchMapsResults)) {\n    return searchMapsResults;\n  }\n  const results = searchMapsResults.results;\n  if (mode === \"step\") {\n    return {\n      context: [toLLMContent(results)],\n    };\n  }\n  return { results };\n}\n\nexport type DescribeInputs = {\n  inputs: Inputs;\n  inputSchema: Schema;\n  asType?: boolean;\n};\n\nasync function describe({ asType, ...inputs }: DescribeInputs) {\n  const isTool = inputs && Object.keys(inputs).length === 1;\n  if (isTool) {\n    return toolSearchMapsDescribe();\n  }\n  const hasWires = \"context\" in (inputs.inputSchema.properties || {});\n  const query: Schema[\"properties\"] = hasWires\n    ? {}\n    : {\n        \"p-query\": {\n          type: \"object\",\n          title: \"Search query\",\n          description: \"Please provide a search query\",\n          behavior: [\n            \"llm-content\",\n            \"config\",\n            \"hint-preview\",\n            \"hint-single-line\",\n          ],\n          default: defaultLLMContent(),\n        },\n      };\n\n  return {\n    inputSchema: {\n      type: \"object\",\n      properties: {\n        context: {\n          type: \"array\",\n          items: { type: \"object\", behavior: [\"llm-content\"] },\n          title: \"Context in\",\n          behavior: [\"main-port\"],\n        },\n        ...query,\n      },\n    } satisfies Schema,\n    outputSchema: {\n      type: \"object\",\n      properties: {\n        context: {\n          type: \"array\",\n          items: { type: \"object\", behavior: [\"llm-content\"] },\n          title: \"Context out\",\n          behavior: [\"main-port\"],\n        },\n      },\n    } satisfies Schema,\n\n    metadata: {\n      icon: \"map-search\",\n      tags: [\"quick-access\", \"tool\", \"component\"],\n      order: 2,\n    },\n  };\n}\n",
+          "code": "/**\n * @fileoverview Add a description for your module here.\n */\n\nimport toolSearchMaps, {\n  type SearchMapsOutputs,\n  describe as toolSearchMapsDescribe,\n} from \"./tool-search-maps\";\nimport { Template } from \"./a2/template\";\nimport { err, ok, toText, toLLMContent, defaultLLMContent } from \"./a2/utils\";\nexport { invoke as default, describe };\n\ntype Inputs =\n  | {\n      context?: LLMContent[];\n      \"p-query\": LLMContent;\n    }\n  | {\n      query: string;\n    };\n\ntype Outputs =\n  | {\n      context: LLMContent[];\n    }\n  | {\n      results: string;\n    };\n\nasync function resolveInput(inputContent: LLMContent): Promise<string> {\n  const template = new Template(inputContent);\n  const substituting = await template.substitute({}, async () => \"\");\n  if (!ok(substituting)) {\n    return substituting.$error;\n  }\n  return toText(substituting);\n}\n\nasync function invoke(inputs: Inputs): Promise<Outcome<Outputs>> {\n  console.log(\"MAPS INPUTS\", inputs);\n  let query: string;\n  let mode: \"step\" | \"tool\";\n  if (\"context\" in inputs) {\n    mode = \"step\";\n    const last = inputs.context?.at(-1);\n    if (last) {\n      query = toText(last);\n    } else {\n      return err(\"Please provide a query\");\n    }\n  } else if (\"p-query\" in inputs) {\n    query = await resolveInput(inputs[\"p-query\"]);\n    mode = \"step\";\n  } else {\n    query = inputs.query;\n    mode = \"tool\";\n  }\n  query = (query || \"\").trim();\n  if (!query) {\n    return err(\"Please provide a query\");\n  }\n\n  console.log(\"Query: \" + query);\n  const searchMapsResults = await toolSearchMaps({ query });\n  if (!ok(searchMapsResults)) {\n    return searchMapsResults;\n  }\n  const results = searchMapsResults.results;\n  if (mode === \"step\") {\n    return {\n      context: [toLLMContent(results)],\n    };\n  }\n  return { results };\n}\n\nexport type DescribeInputs = {\n  inputs: Inputs;\n  inputSchema: Schema;\n  asType?: boolean;\n};\n\nasync function describe({ asType, ...inputs }: DescribeInputs) {\n  const isTool = inputs && Object.keys(inputs).length === 1;\n  if (isTool) {\n    return toolSearchMapsDescribe();\n  }\n  const hasWires = \"context\" in (inputs.inputSchema.properties || {});\n  const query: Schema[\"properties\"] = hasWires\n    ? {}\n    : {\n        \"p-query\": {\n          type: \"object\",\n          title: \"Search query\",\n          description: \"Please provide a search query\",\n          behavior: [\n            \"llm-content\",\n            \"config\",\n            \"hint-preview\",\n            \"hint-single-line\",\n          ],\n          default: defaultLLMContent(),\n        },\n      };\n\n  return {\n    inputSchema: {\n      type: \"object\",\n      properties: {\n        context: {\n          type: \"array\",\n          items: { type: \"object\", behavior: [\"llm-content\"] },\n          title: \"Context in\",\n          behavior: [\"main-port\"],\n        },\n        ...query,\n      },\n    } satisfies Schema,\n    outputSchema: {\n      type: \"object\",\n      properties: {\n        context: {\n          type: \"array\",\n          items: { type: \"object\", behavior: [\"llm-content\"] },\n          title: \"Context out\",\n          behavior: [\"main-port\"],\n        },\n      },\n    } satisfies Schema,\n\n    metadata: {\n      icon: \"map-search\",\n      tags: [\"quick-access\", \"tool\", \"component\"],\n      order: 2,\n    },\n  };\n}\n",
           "language": "typescript"
         },
         "description": "Search for location information.",
@@ -115,11 +115,11 @@
       }
     },
     "get-weather": {
-      "code": "/**\n * @fileoverview Searches weather information on Google Search.\n */\nimport { ok, err, toLLMContent, toText, defaultLLMContent } from \"./a2/utils\";\nimport getWeather, { describe as getWeatherDescriber, } from \"./get-weather-tool\";\nimport { Template } from \"./a2/template\";\nexport { invoke as default, describe };\nasync function resolveInput(inputContent) {\n    const template = new Template(inputContent);\n    const substituting = await template.substitute({}, async () => \"\");\n    if (!ok(substituting)) {\n        return substituting.$error;\n    }\n    return toText(substituting);\n}\nasync function invoke(inputs) {\n    let location;\n    let mode;\n    if (\"context\" in inputs) {\n        mode = \"step\";\n        const last = inputs.context?.at(-1);\n        if (last) {\n            location = toText(last);\n        }\n        else {\n            return err(\"Please provide a location\");\n        }\n    }\n    else if (\"p-location\" in inputs) {\n        location = await resolveInput(inputs[\"p-location\"]);\n        mode = \"step\";\n    }\n    else {\n        location = inputs.location;\n        mode = \"tool\";\n    }\n    location = (location || \"\").trim();\n    if (!location) {\n        return err(\"Please provide a location\");\n    }\n    console.log(\"Location: \" + location);\n    const weatherResult = await getWeather({ location });\n    if (!ok(weatherResult)) {\n        return weatherResult;\n    }\n    return {\n        context: [\n            toLLMContent(`Location: ${location}\\n\\n Weather information: ${toText(weatherResult)}`),\n        ],\n    };\n}\nasync function describe({ asType, ...inputs }) {\n    const isTool = inputs && Object.keys(inputs).length === 1;\n    if (isTool) {\n        return getWeatherDescriber();\n    }\n    const hasWires = \"context\" in (inputs.inputSchema.properties || {});\n    const location = hasWires\n        ? {}\n        : {\n            \"p-location\": {\n                type: \"object\",\n                title: \"Location\",\n                description: \"Please provide the location for which to get current weather\",\n                behavior: [\n                    \"llm-content\",\n                    \"config\",\n                    \"hint-preview\",\n                    \"hint-single-line\",\n                ],\n                default: defaultLLMContent(),\n            },\n        };\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                ...location,\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"hint-text\", \"main-port\"],\n                },\n            },\n        },\n        metadata: {\n            icon: \"sunny\",\n            tags: [\"quick-access\", \"tool\", \"component\"],\n            order: 5,\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Add a description for your module here.\n */\nimport { ok, err, toLLMContent, toText, defaultLLMContent } from \"./a2/utils\";\nimport getWeather, { describe as getWeatherDescriber, } from \"./get-weather-tool\";\nimport { Template } from \"./a2/template\";\nexport { invoke as default, describe };\nasync function resolveInput(inputContent) {\n    const template = new Template(inputContent);\n    const substituting = await template.substitute({}, async () => \"\");\n    if (!ok(substituting)) {\n        return substituting.$error;\n    }\n    return toText(substituting);\n}\nasync function invoke(inputs) {\n    let location;\n    let mode;\n    if (\"context\" in inputs) {\n        mode = \"step\";\n        const last = inputs.context?.at(-1);\n        if (last) {\n            location = toText(last);\n        }\n        else {\n            return err(\"Please provide a location\");\n        }\n    }\n    else if (\"p-location\" in inputs) {\n        location = await resolveInput(inputs[\"p-location\"]);\n        mode = \"step\";\n    }\n    else {\n        location = inputs.location;\n        mode = \"tool\";\n    }\n    location = (location || \"\").trim();\n    if (!location) {\n        return err(\"Please provide a location\");\n    }\n    console.log(\"Location: \" + location);\n    const weatherResult = await getWeather({ location });\n    if (!ok(weatherResult)) {\n        return weatherResult;\n    }\n    return {\n        context: [\n            toLLMContent(`Location: ${location}\\n\\n Weather information: ${toText(weatherResult)}`),\n        ],\n    };\n}\nasync function describe({ asType, ...inputs }) {\n    const isTool = inputs && Object.keys(inputs).length === 1;\n    if (isTool) {\n        return getWeatherDescriber();\n    }\n    const hasWires = \"context\" in (inputs.inputSchema.properties || {});\n    const location = hasWires\n        ? {}\n        : {\n            \"p-location\": {\n                type: \"object\",\n                title: \"Location\",\n                description: \"Please provide the location for which to get current weather\",\n                behavior: [\n                    \"llm-content\",\n                    \"config\",\n                    \"hint-preview\",\n                    \"hint-single-line\",\n                ],\n                default: defaultLLMContent(),\n            },\n        };\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                ...location,\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"hint-text\", \"main-port\"],\n                },\n            },\n        },\n        metadata: {\n            icon: \"sunny\",\n            tags: [\"quick-access\", \"tool\", \"component\"],\n            order: 5,\n        },\n    };\n}\n",
       "metadata": {
         "title": "Get Weather",
         "source": {
-          "code": "/**\n * @fileoverview Searches weather information on Google Search.\n */\n\nimport { ok, err, toLLMContent, toText, defaultLLMContent } from \"./a2/utils\";\nimport getWeather, {\n  describe as getWeatherDescriber,\n  type WeatherInputs,\n} from \"./get-weather-tool\";\nimport { Template } from \"./a2/template\";\n\nexport { invoke as default, describe };\n\n/**\n * Every tool operates in one of three modes:\n * - `tool` -- when invoked as a tool by an LLM\n * - `step` -- when used as a step in a visual editor\n * - `invoke` -- when invoked directly as a module (by another module)\n */\nexport type ToolMode = \"tool\" | \"step\" | \"invoke\";\n\nasync function resolveInput(inputContent: LLMContent): Promise<string> {\n  const template = new Template(inputContent);\n  const substituting = await template.substitute({}, async () => \"\");\n  if (!ok(substituting)) {\n    return substituting.$error;\n  }\n  return toText(substituting);\n}\n\nexport type Inputs =\n  | {\n      context?: LLMContent[];\n      \"p-location\": LLMContent;\n    }\n  | WeatherInputs;\n\nexport type Outputs = {\n  context: LLMContent[];\n};\n\nasync function invoke(inputs: Inputs): Promise<Outcome<Outputs>> {\n  let location;\n  let mode: ToolMode;\n  if (\"context\" in inputs) {\n    mode = \"step\";\n    const last = inputs.context?.at(-1);\n    if (last) {\n      location = toText(last);\n    } else {\n      return err(\"Please provide a location\");\n    }\n  } else if (\"p-location\" in inputs) {\n    location = await resolveInput(inputs[\"p-location\"]);\n    mode = \"step\";\n  } else {\n    location = inputs.location;\n    mode = \"tool\";\n  }\n  location = (location || \"\").trim();\n  if (!location) {\n    return err(\"Please provide a location\");\n  }\n  console.log(\"Location: \" + location);\n  const weatherResult = await getWeather({ location });\n  if (!ok(weatherResult)) {\n    return weatherResult;\n  }\n  return {\n    context: [\n      toLLMContent(\n        `Location: ${location}\\n\\n Weather information: ${toText(weatherResult)}`\n      ),\n    ],\n  };\n}\n\nexport type DescribeInputs = {\n  inputs: Inputs;\n  inputSchema: Schema;\n  asType: boolean;\n};\n\nasync function describe({ asType, ...inputs }: DescribeInputs) {\n  const isTool = inputs && Object.keys(inputs).length === 1;\n  if (isTool) {\n    return getWeatherDescriber();\n  }\n  const hasWires = \"context\" in (inputs.inputSchema.properties || {});\n  const location: Schema[\"properties\"] = hasWires\n    ? {}\n    : {\n        \"p-location\": {\n          type: \"object\",\n          title: \"Location\",\n          description:\n            \"Please provide the location for which to get current weather\",\n          behavior: [\n            \"llm-content\",\n            \"config\",\n            \"hint-preview\",\n            \"hint-single-line\",\n          ],\n          default: defaultLLMContent(),\n        },\n      };\n  return {\n    inputSchema: {\n      type: \"object\",\n      properties: {\n        context: {\n          type: \"array\",\n          items: { type: \"object\", behavior: [\"llm-content\"] },\n          title: \"Context in\",\n          behavior: [\"main-port\"],\n        },\n        ...location,\n      },\n    } satisfies Schema,\n    outputSchema: {\n      type: \"object\",\n      properties: {\n        context: {\n          type: \"array\",\n          items: { type: \"object\", behavior: [\"llm-content\"] },\n          title: \"Context out\",\n          behavior: [\"hint-text\", \"main-port\"],\n        },\n      },\n    } satisfies Schema,\n    metadata: {\n      icon: \"sunny\",\n      tags: [\"quick-access\", \"tool\", \"component\"],\n      order: 5,\n    },\n  };\n}\n",
+          "code": "/**\n * @fileoverview Add a description for your module here.\n */\n\nimport { ok, err, toLLMContent, toText, defaultLLMContent } from \"./a2/utils\";\nimport getWeather, {\n  describe as getWeatherDescriber,\n  type WeatherInputs,\n} from \"./get-weather-tool\";\nimport { Template } from \"./a2/template\";\n\nexport { invoke as default, describe };\n\n/**\n * Every tool operates in one of three modes:\n * - `tool` -- when invoked as a tool by an LLM\n * - `step` -- when used as a step in a visual editor\n * - `invoke` -- when invoked directly as a module (by another module)\n */\nexport type ToolMode = \"tool\" | \"step\" | \"invoke\";\n\nasync function resolveInput(inputContent: LLMContent): Promise<string> {\n  const template = new Template(inputContent);\n  const substituting = await template.substitute({}, async () => \"\");\n  if (!ok(substituting)) {\n    return substituting.$error;\n  }\n  return toText(substituting);\n}\n\nexport type Inputs =\n  | {\n      context?: LLMContent[];\n      \"p-location\": LLMContent;\n    }\n  | WeatherInputs;\n\nexport type Outputs = {\n  context: LLMContent[];\n};\n\nasync function invoke(inputs: Inputs): Promise<Outcome<Outputs>> {\n  let location;\n  let mode: ToolMode;\n  if (\"context\" in inputs) {\n    mode = \"step\";\n    const last = inputs.context?.at(-1);\n    if (last) {\n      location = toText(last);\n    } else {\n      return err(\"Please provide a location\");\n    }\n  } else if (\"p-location\" in inputs) {\n    location = await resolveInput(inputs[\"p-location\"]);\n    mode = \"step\";\n  } else {\n    location = inputs.location;\n    mode = \"tool\";\n  }\n  location = (location || \"\").trim();\n  if (!location) {\n    return err(\"Please provide a location\");\n  }\n  console.log(\"Location: \" + location);\n  const weatherResult = await getWeather({ location });\n  if (!ok(weatherResult)) {\n    return weatherResult;\n  }\n  return {\n    context: [\n      toLLMContent(\n        `Location: ${location}\\n\\n Weather information: ${toText(weatherResult)}`\n      ),\n    ],\n  };\n}\n\nexport type DescribeInputs = {\n  inputs: Inputs;\n  inputSchema: Schema;\n  asType: boolean;\n};\n\nasync function describe({ asType, ...inputs }: DescribeInputs) {\n  const isTool = inputs && Object.keys(inputs).length === 1;\n  if (isTool) {\n    return getWeatherDescriber();\n  }\n  const hasWires = \"context\" in (inputs.inputSchema.properties || {});\n  const location: Schema[\"properties\"] = hasWires\n    ? {}\n    : {\n        \"p-location\": {\n          type: \"object\",\n          title: \"Location\",\n          description:\n            \"Please provide the location for which to get current weather\",\n          behavior: [\n            \"llm-content\",\n            \"config\",\n            \"hint-preview\",\n            \"hint-single-line\",\n          ],\n          default: defaultLLMContent(),\n        },\n      };\n  return {\n    inputSchema: {\n      type: \"object\",\n      properties: {\n        context: {\n          type: \"array\",\n          items: { type: \"object\", behavior: [\"llm-content\"] },\n          title: \"Context in\",\n          behavior: [\"main-port\"],\n        },\n        ...location,\n      },\n    } satisfies Schema,\n    outputSchema: {\n      type: \"object\",\n      properties: {\n        context: {\n          type: \"array\",\n          items: { type: \"object\", behavior: [\"llm-content\"] },\n          title: \"Context out\",\n          behavior: [\"hint-text\", \"main-port\"],\n        },\n      },\n    } satisfies Schema,\n    metadata: {\n      icon: \"sunny\",\n      tags: [\"quick-access\", \"tool\", \"component\"],\n      order: 5,\n    },\n  };\n}\n",
           "language": "typescript"
         },
         "description": "Searches weather information on Google Search.",
@@ -127,7 +127,7 @@
       }
     },
     "get-weather-tool": {
-      "code": "/**\n * @fileoverview The actual tool that gets the weather.\n */\nimport fetch from \"@fetch\";\nexport { invoke as default, describe };\nimport { ok, err } from \"./a2/utils\";\nimport { executeTool } from \"./a2/step-executor\";\nimport { toLLMContent } from \"./a2/utils\";\nconst USE_METEO = false;\nconst WEATHER_CODES = {\n    // Individual codes\n    \"0\": {\n        description: \"Clear sky\",\n        category: \"clear\",\n    },\n    // Ranges for cloud coverage\n    \"1\": {\n        description: \"Mainly clear\",\n        category: \"partlyCloudy\",\n    },\n    \"2\": {\n        description: \"Partly cloudy\",\n        category: \"partlyCloudy\",\n    },\n    \"3\": {\n        description: \"Overcast\",\n        category: \"cloudy\",\n    },\n    // Fog conditions\n    \"45\": {\n        description: \"Fog\",\n        category: \"fog\",\n    },\n    \"48\": {\n        description: \"Depositing rime fog\",\n        category: \"fog\",\n    },\n    // Drizzle intensities\n    \"51\": {\n        description: \"Light drizzle\",\n        category: \"drizzle\",\n        intensity: \"light\",\n    },\n    \"53\": {\n        description: \"Moderate drizzle\",\n        category: \"drizzle\",\n        intensity: \"moderate\",\n    },\n    \"55\": {\n        description: \"Dense drizzle\",\n        category: \"drizzle\",\n        intensity: \"heavy\",\n    },\n    // Freezing drizzle\n    \"56\": {\n        description: \"Light freezing drizzle\",\n        category: \"freezingDrizzle\",\n        intensity: \"light\",\n    },\n    \"57\": {\n        description: \"Dense freezing drizzle\",\n        category: \"freezingDrizzle\",\n        intensity: \"heavy\",\n    },\n    // Rain intensities\n    \"61\": {\n        description: \"Slight rain\",\n        category: \"rain\",\n        intensity: \"light\",\n    },\n    \"63\": {\n        description: \"Moderate rain\",\n        category: \"rain\",\n        intensity: \"moderate\",\n    },\n    \"65\": {\n        description: \"Heavy rain\",\n        category: \"rain\",\n        intensity: \"heavy\",\n    },\n    // Freezing rain\n    \"66\": {\n        description: \"Light freezing rain\",\n        category: \"freezingRain\",\n        intensity: \"light\",\n    },\n    \"67\": {\n        description: \"Heavy freezing rain\",\n        category: \"freezingRain\",\n        intensity: \"heavy\",\n    },\n    // Snow intensities\n    \"71\": {\n        description: \"Slight snow fall\",\n        category: \"snow\",\n        intensity: \"light\",\n    },\n    \"73\": {\n        description: \"Moderate snow fall\",\n        category: \"snow\",\n        intensity: \"moderate\",\n    },\n    \"75\": {\n        description: \"Heavy snow fall\",\n        category: \"snow\",\n        intensity: \"heavy\",\n    },\n    // Snow grains\n    \"77\": {\n        description: \"Snow grains\",\n        category: \"snowGrains\",\n    },\n    // Rain showers\n    \"80\": {\n        description: \"Slight rain showers\",\n        category: \"rainShowers\",\n        intensity: \"light\",\n    },\n    \"81\": {\n        description: \"Moderate rain showers\",\n        category: \"rainShowers\",\n        intensity: \"moderate\",\n    },\n    \"82\": {\n        description: \"Violent rain showers\",\n        category: \"rainShowers\",\n        intensity: \"violent\",\n    },\n    // Snow showers\n    \"85\": {\n        description: \"Slight snow showers\",\n        category: \"snowShowers\",\n        intensity: \"light\",\n    },\n    \"86\": {\n        description: \"Heavy snow showers\",\n        category: \"snowShowers\",\n        intensity: \"heavy\",\n    },\n    // Thunderstorm\n    \"95\": {\n        description: \"Thunderstorm\",\n        category: \"thunderstorm\",\n        intensity: \"moderate\",\n    },\n    // Thunderstorm with hail\n    \"96\": {\n        description: \"Thunderstorm with slight hail\",\n        category: \"thunderstormWithHail\",\n        intensity: \"light\",\n    },\n    \"99\": {\n        description: \"Thunderstorm with heavy hail\",\n        category: \"thunderstormWithHail\",\n        intensity: \"heavy\",\n    },\n};\nfunction geocodingUrl(location) {\n    return `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(location)}&count=10&language=en&format=json`;\n}\nfunction weatherUrl(latitude, longitude) {\n    // current=temperature_2m,is_day,precipitation,rain,showers,snowfall&temperature_unit=fahrenheit&wind_speed_unit=mph&precipitation_unit=inch&timezone=auto&forecast_days=1\n    return `https://api.open-meteo.com/v1/forecast?latitude=${encodeURIComponent(latitude)}&longitude=${encodeURIComponent(longitude)}&current=weather_code,temperature_2m,is_day,precipitation,rain,showers,snowfall&temperature_unit=fahrenheit&wind_speed_unit=mph&precipitation_unit=inch&timezone=auto&forecast_days=1`;\n}\nfunction getConditions(weather_code) {\n    const code = `${weather_code}`;\n    const conditions = WEATHER_CODES[code];\n    return conditions;\n}\nasync function invoke({ location, }) {\n    if (!USE_METEO) {\n        const executing = await executeTool(\"get_weather\", {\n            location,\n        });\n        if (!ok(executing))\n            return executing;\n        return toLLMContent(executing);\n    }\n    const geocodingResponse = await fetch({ url: geocodingUrl(location) });\n    if (\"$error\" in geocodingResponse) {\n        return { $error: geocodingResponse.$error };\n    }\n    const geocodingResults = geocodingResponse.response;\n    if (!geocodingResults.results) {\n        return { $error: `No results for location: \"${location}\"` };\n    }\n    const { latitude, longitude } = geocodingResults.results.at(0) || {};\n    if (!latitude || !longitude) {\n        return { $error: `No latitude/longitude for location: \"${location}\"` };\n    }\n    const weatherResponse = await fetch({ url: weatherUrl(latitude, longitude) });\n    if (\"$error\" in weatherResponse) {\n        return { $error: weatherResponse.$error };\n    }\n    const { current } = weatherResponse.response;\n    const precipitation = current.precipitation\n        ? `${current.precipitation} inches`\n        : \"none\";\n    const weather = {\n        location,\n        conditions: getConditions(current.weather_code),\n        time: current.time,\n        is_day: current.is_day == 1,\n        precipitation,\n        temperature: `${current.temperature_2m} F`,\n    };\n    if (current.showers) {\n        weather.showers = true;\n    }\n    if (current.snowfall) {\n        weather.snowfall = true;\n    }\n    if (current.rain) {\n        weather.rain = true;\n    }\n    const result = JSON.stringify(weather, null, 2);\n    return toLLMContent(result);\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                location: {\n                    type: \"string\",\n                    title: \"Location\",\n                    description: \"The name of the city\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                weather: {\n                    type: \"object\",\n                    title: \"Current Weather\",\n                },\n            },\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview The actual tool that gets the weather.\n */\nimport fetch from \"@fetch\";\nexport { invoke as default, describe };\nimport { ok } from \"./a2/utils\";\nimport { executeTool } from \"./a2/step-executor\";\nimport { toLLMContent } from \"./a2/utils\";\nconst USE_METEO = false;\nconst WEATHER_CODES = {\n    // Individual codes\n    \"0\": {\n        description: \"Clear sky\",\n        category: \"clear\",\n    },\n    // Ranges for cloud coverage\n    \"1\": {\n        description: \"Mainly clear\",\n        category: \"partlyCloudy\",\n    },\n    \"2\": {\n        description: \"Partly cloudy\",\n        category: \"partlyCloudy\",\n    },\n    \"3\": {\n        description: \"Overcast\",\n        category: \"cloudy\",\n    },\n    // Fog conditions\n    \"45\": {\n        description: \"Fog\",\n        category: \"fog\",\n    },\n    \"48\": {\n        description: \"Depositing rime fog\",\n        category: \"fog\",\n    },\n    // Drizzle intensities\n    \"51\": {\n        description: \"Light drizzle\",\n        category: \"drizzle\",\n        intensity: \"light\",\n    },\n    \"53\": {\n        description: \"Moderate drizzle\",\n        category: \"drizzle\",\n        intensity: \"moderate\",\n    },\n    \"55\": {\n        description: \"Dense drizzle\",\n        category: \"drizzle\",\n        intensity: \"heavy\",\n    },\n    // Freezing drizzle\n    \"56\": {\n        description: \"Light freezing drizzle\",\n        category: \"freezingDrizzle\",\n        intensity: \"light\",\n    },\n    \"57\": {\n        description: \"Dense freezing drizzle\",\n        category: \"freezingDrizzle\",\n        intensity: \"heavy\",\n    },\n    // Rain intensities\n    \"61\": {\n        description: \"Slight rain\",\n        category: \"rain\",\n        intensity: \"light\",\n    },\n    \"63\": {\n        description: \"Moderate rain\",\n        category: \"rain\",\n        intensity: \"moderate\",\n    },\n    \"65\": {\n        description: \"Heavy rain\",\n        category: \"rain\",\n        intensity: \"heavy\",\n    },\n    // Freezing rain\n    \"66\": {\n        description: \"Light freezing rain\",\n        category: \"freezingRain\",\n        intensity: \"light\",\n    },\n    \"67\": {\n        description: \"Heavy freezing rain\",\n        category: \"freezingRain\",\n        intensity: \"heavy\",\n    },\n    // Snow intensities\n    \"71\": {\n        description: \"Slight snow fall\",\n        category: \"snow\",\n        intensity: \"light\",\n    },\n    \"73\": {\n        description: \"Moderate snow fall\",\n        category: \"snow\",\n        intensity: \"moderate\",\n    },\n    \"75\": {\n        description: \"Heavy snow fall\",\n        category: \"snow\",\n        intensity: \"heavy\",\n    },\n    // Snow grains\n    \"77\": {\n        description: \"Snow grains\",\n        category: \"snowGrains\",\n    },\n    // Rain showers\n    \"80\": {\n        description: \"Slight rain showers\",\n        category: \"rainShowers\",\n        intensity: \"light\",\n    },\n    \"81\": {\n        description: \"Moderate rain showers\",\n        category: \"rainShowers\",\n        intensity: \"moderate\",\n    },\n    \"82\": {\n        description: \"Violent rain showers\",\n        category: \"rainShowers\",\n        intensity: \"violent\",\n    },\n    // Snow showers\n    \"85\": {\n        description: \"Slight snow showers\",\n        category: \"snowShowers\",\n        intensity: \"light\",\n    },\n    \"86\": {\n        description: \"Heavy snow showers\",\n        category: \"snowShowers\",\n        intensity: \"heavy\",\n    },\n    // Thunderstorm\n    \"95\": {\n        description: \"Thunderstorm\",\n        category: \"thunderstorm\",\n        intensity: \"moderate\",\n    },\n    // Thunderstorm with hail\n    \"96\": {\n        description: \"Thunderstorm with slight hail\",\n        category: \"thunderstormWithHail\",\n        intensity: \"light\",\n    },\n    \"99\": {\n        description: \"Thunderstorm with heavy hail\",\n        category: \"thunderstormWithHail\",\n        intensity: \"heavy\",\n    },\n};\nfunction geocodingUrl(location) {\n    return `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(location)}&count=10&language=en&format=json`;\n}\nfunction weatherUrl(latitude, longitude) {\n    // current=temperature_2m,is_day,precipitation,rain,showers,snowfall&temperature_unit=fahrenheit&wind_speed_unit=mph&precipitation_unit=inch&timezone=auto&forecast_days=1\n    return `https://api.open-meteo.com/v1/forecast?latitude=${encodeURIComponent(latitude)}&longitude=${encodeURIComponent(longitude)}&current=weather_code,temperature_2m,is_day,precipitation,rain,showers,snowfall&temperature_unit=fahrenheit&wind_speed_unit=mph&precipitation_unit=inch&timezone=auto&forecast_days=1`;\n}\nfunction getConditions(weather_code) {\n    const code = `${weather_code}`;\n    const conditions = WEATHER_CODES[code];\n    return conditions;\n}\nasync function invoke({ location, }) {\n    if (!USE_METEO) {\n        const executing = await executeTool(\"get_weather\", {\n            location,\n        });\n        if (!ok(executing))\n            return executing;\n        return toLLMContent(executing);\n    }\n    const geocodingResponse = await fetch({ url: geocodingUrl(location) });\n    if (\"$error\" in geocodingResponse) {\n        return { $error: geocodingResponse.$error };\n    }\n    const geocodingResults = geocodingResponse.response;\n    if (!geocodingResults.results) {\n        return { $error: `No results for location: \"${location}\"` };\n    }\n    const { latitude, longitude } = geocodingResults.results.at(0) || {};\n    if (!latitude || !longitude) {\n        return { $error: `No latitude/longitude for location: \"${location}\"` };\n    }\n    const weatherResponse = await fetch({ url: weatherUrl(latitude, longitude) });\n    if (\"$error\" in weatherResponse) {\n        return { $error: weatherResponse.$error };\n    }\n    const { current } = weatherResponse.response;\n    const precipitation = current.precipitation\n        ? `${current.precipitation} inches`\n        : \"none\";\n    const weather = {\n        location,\n        conditions: getConditions(current.weather_code),\n        time: current.time,\n        is_day: current.is_day == 1,\n        precipitation,\n        temperature: `${current.temperature_2m} F`,\n    };\n    if (current.showers) {\n        weather.showers = true;\n    }\n    if (current.snowfall) {\n        weather.snowfall = true;\n    }\n    if (current.rain) {\n        weather.rain = true;\n    }\n    const result = JSON.stringify(weather, null, 2);\n    return toLLMContent(result);\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                location: {\n                    type: \"string\",\n                    title: \"Location\",\n                    description: \"The name of the city\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                weather: {\n                    type: \"object\",\n                    title: \"Current Weather\",\n                },\n            },\n        },\n    };\n}\n",
       "metadata": {
         "title": "get-weather-tool",
         "source": {
@@ -151,7 +151,7 @@
       }
     },
     "tool-search-web": {
-      "code": "/**\n * @fileoverview The internal-only implementation of the Search Web tool.\n */\nimport { ToolManager } from \"./a2/tool-manager\";\nimport { GeminiPrompt } from \"./a2/gemini-prompt\";\nimport { ok, err, toText } from \"./a2/utils\";\nimport { executeTool } from \"./a2/step-executor\";\nimport secrets from \"@secrets\";\nimport fetch from \"@fetch\";\nexport { invoke as default, describe };\nasync function generateSummary(query) {\n    const toolManager = new ToolManager();\n    toolManager.addSearch();\n    const result = await new GeminiPrompt({\n        model: \"gemini-2.0-flash\",\n        body: {\n            contents: [{ parts: [{ text: query }] }],\n            tools: toolManager.list(),\n        },\n    }, toolManager).invoke();\n    if (!ok(result)) {\n        return result;\n    }\n    let results = toText(result.last);\n    const chunks = result.candidate?.groundingMetadata?.groundingChunks?.map((chunk) => {\n        const { title, uri } = chunk.web;\n        return `- [${title}](${uri})`;\n    }) || [];\n    if (chunks.length) {\n        results += `\\n## References:\\n${chunks.join(\"\\n\")}\\n`;\n    }\n    return `\\n## Summary\\n${results}`;\n}\nfunction formatSearchResults(results) {\n    return `## Raw Search Results\n\n${results.items\n        .map((item) => {\n        return `- [${item.title}](${item.link})\n${item.snippet}\n`;\n    })\n        .join(\"\\n\\n\")}\n`;\n}\nfunction formatBackendSearchResults(results) {\n    return `## Search Results\n\n    ${results\n        .map((result) => {\n        return `## Source: ${result.url}\nSource content:\n\n${result.webpage_text_content}\n`;\n    })\n        .join(\"\\n\\n\")}\n`;\n}\nasync function getSearchLinks(query) {\n    const results = await executeTool(\"google_search\", {\n        query,\n    });\n    if (!ok(results))\n        return results;\n    return formatBackendSearchResults(results);\n}\nasync function invoke({ query, }) {\n    const [summary, links] = await Promise.all([\n        generateSummary(query),\n        getSearchLinks(query),\n    ]);\n    if (!ok(summary)) {\n        return summary;\n    }\n    if (!ok(links)) {\n        return links;\n    }\n    return { results: `Query: ${query}\\n${summary}\\n${links}` };\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                query: {\n                    type: \"string\",\n                    title: \"Query\",\n                    description: \"The query to use with which to search the Web\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                },\n            },\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview The internal-only implementation of the Search Web tool.\n */\nimport { ToolManager } from \"./a2/tool-manager\";\nimport { GeminiPrompt } from \"./a2/gemini-prompt\";\nimport { ok, toText } from \"./a2/utils\";\nimport { executeTool } from \"./a2/step-executor\";\nexport { invoke as default, describe };\nasync function generateSummary(query) {\n    const toolManager = new ToolManager();\n    toolManager.addSearch();\n    const result = await new GeminiPrompt({\n        model: \"gemini-2.0-flash\",\n        body: {\n            contents: [{ parts: [{ text: query }] }],\n            tools: toolManager.list(),\n        },\n    }, toolManager).invoke();\n    if (!ok(result)) {\n        return result;\n    }\n    let results = toText(result.last);\n    const chunks = result.candidate?.groundingMetadata?.groundingChunks?.map((chunk) => {\n        const { title, uri } = chunk.web;\n        return `- [${title}](${uri})`;\n    }) || [];\n    if (chunks.length) {\n        results += `\\n## References:\\n${chunks.join(\"\\n\")}\\n`;\n    }\n    return `\\n## Summary\\n${results}`;\n}\nfunction formatSearchResults(results) {\n    return `## Raw Search Results\n\n${results.items\n        .map((item) => {\n        return `- [${item.title}](${item.link})\n${item.snippet}\n`;\n    })\n        .join(\"\\n\\n\")}\n`;\n}\nfunction formatBackendSearchResults(results) {\n    return `## Search Results\n\n    ${results\n        .map((result) => {\n        return `## Source: ${result.url}\nSource content:\n\n${result.webpage_text_content}\n`;\n    })\n        .join(\"\\n\\n\")}\n`;\n}\nasync function getSearchLinks(query) {\n    const results = await executeTool(\"google_search\", {\n        query,\n    });\n    if (!ok(results))\n        return results;\n    return formatBackendSearchResults(results);\n}\nasync function invoke({ query, }) {\n    const [summary, links] = await Promise.all([\n        generateSummary(query),\n        getSearchLinks(query),\n    ]);\n    if (!ok(summary)) {\n        return summary;\n    }\n    if (!ok(links)) {\n        return links;\n    }\n    return { results: `Query: ${query}\\n${summary}\\n${links}` };\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                query: {\n                    type: \"string\",\n                    title: \"Query\",\n                    description: \"The query to use with which to search the Web\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                },\n            },\n        },\n    };\n}\n",
       "metadata": {
         "title": "tool-search-web",
         "source": {
@@ -163,7 +163,7 @@
       }
     },
     "tool-search-maps": {
-      "code": "/**\n * @fileoverview The guts of the Search Maps tool.\n */\nimport { ok, err } from \"./a2/utils\";\nimport { executeTool } from \"./a2/step-executor\";\nexport { invoke as default, describe };\nfunction formatResults(query, results) {\n    // If the result is already in string format, not SearchMap results, return as is.\n    if (typeof results == \"string\") {\n        return `Search Query: ${query}\n## Google Places Search Results\n${results}\n    `;\n    }\n    return `Search Query: ${query}\n\n\n## Google Places Search Results\n\n${results.places\n        .map((place) => {\n        const title = place.websiteUri\n            ? `[${place.displayName.text}](${place.websiteUri})`\n            : place.displayName.text;\n        return `- ${title}\\n\n  ${place.editorialSummary?.text || \"\"} \n  Address: ${place.formattedAddress}\n  User Rating: ${place.rating} (${place.userRatingCount} reviews)\n  `;\n    })\n        .join(\"\\n\\n\")}\n`;\n}\nasync function invoke({ query, }) {\n    const executing = await executeTool(\"map_search\", {\n        query,\n    });\n    if (!ok(executing))\n        return executing;\n    const results = formatResults(query, executing);\n    return { results };\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                query: {\n                    type: \"string\",\n                    title: \"Query\",\n                    description: \"Google Places API search query, typically formulated as [type of place] near [location]\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                results: {\n                    type: \"string\",\n                    title: \"Search Results\",\n                },\n            },\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview The guts of the Search Maps tool.\n */\nimport { ok } from \"./a2/utils\";\nimport { executeTool } from \"./a2/step-executor\";\nexport { invoke as default, describe };\nfunction formatResults(query, results) {\n    // If the result is already in string format, not SearchMap results, return as is.\n    if (typeof results == \"string\") {\n        return `Search Query: ${query}\n## Google Places Search Results\n${results}\n    `;\n    }\n    return `Search Query: ${query}\n\n\n## Google Places Search Results\n\n${results.places\n        .map((place) => {\n        const title = place.websiteUri\n            ? `[${place.displayName.text}](${place.websiteUri})`\n            : place.displayName.text;\n        return `- ${title}\\n\n  ${place.editorialSummary?.text || \"\"} \n  Address: ${place.formattedAddress}\n  User Rating: ${place.rating} (${place.userRatingCount} reviews)\n  `;\n    })\n        .join(\"\\n\\n\")}\n`;\n}\nasync function invoke({ query, }) {\n    const executing = await executeTool(\"map_search\", {\n        query,\n    });\n    if (!ok(executing))\n        return executing;\n    const results = formatResults(query, executing);\n    return { results };\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                query: {\n                    type: \"string\",\n                    title: \"Query\",\n                    description: \"Google Places API search query, typically formulated as [type of place] near [location]\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                results: {\n                    type: \"string\",\n                    title: \"Search Results\",\n                },\n            },\n        },\n    };\n}\n",
       "metadata": {
         "title": "tool-search-maps",
         "source": {
@@ -175,7 +175,7 @@
       }
     },
     "tool-search-enterprise": {
-      "code": "/**\n * @fileoverview The guts of the Search Enterprise tool.\n */\nimport { ok, err } from \"./a2/utils\";\nimport { executeTool } from \"./a2/step-executor\";\nexport { invoke as default, describe };\nasync function invoke({ query, search_engine_resource_name, }) {\n    const executing = await executeTool(\"enterprise_search\", {\n        query,\n        search_engine_resource_name,\n    });\n    if (!ok(executing))\n        return executing;\n    return { results: executing };\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                query: {\n                    type: \"string\",\n                    title: \"Query\",\n                    description: \"The search query\",\n                },\n                search_engine_resource_name: {\n                    type: \"string\",\n                    title: \"Search Engine Resource Name [Optional]\",\n                    description: \"An optional resource name for the search backend to use\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                results: {\n                    type: \"string\",\n                    title: \"Search Results\",\n                },\n            },\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview The guts of the Search Enterprise tool.\n */\nimport { ok } from \"./a2/utils\";\nimport { executeTool } from \"./a2/step-executor\";\nexport { invoke as default, describe };\nasync function invoke({ query, search_engine_resource_name, }) {\n    const executing = await executeTool(\"enterprise_search\", {\n        query,\n        search_engine_resource_name,\n    });\n    if (!ok(executing))\n        return executing;\n    return { results: executing };\n}\nasync function describe() {\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                query: {\n                    type: \"string\",\n                    title: \"Query\",\n                    description: \"The search query\",\n                },\n                search_engine_resource_name: {\n                    type: \"string\",\n                    title: \"Search Engine Resource Name [Optional]\",\n                    description: \"An optional resource name for the search backend to use\",\n                },\n            },\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                results: {\n                    type: \"string\",\n                    title: \"Search Results\",\n                },\n            },\n        },\n    };\n}\n",
       "metadata": {
         "title": "tool-search-enterprise",
         "source": {
diff --git a/packages/a2/bgl/video-generator.bgl.json b/packages/a2/bgl/video-generator.bgl.json
index e5f5330e865..d528e82b66c 100644
--- a/packages/a2/bgl/video-generator.bgl.json
+++ b/packages/a2/bgl/video-generator.bgl.json
@@ -51,7 +51,7 @@
   },
   "modules": {
     "main": {
-      "code": "/**\n * @fileoverview Generates video output using supplied context.\n */\nimport gemini, { defaultSafetySettings, } from \"./a2/gemini\";\nimport { err, ok, llm, isStoredData, toTextConcat, joinContent, toLLMContent, toInlineReference, toLLMContentInline, toLLMContentStored, toText, toInlineData, extractMediaData, extractTextData, defaultLLMContent, } from \"./a2/utils\";\nimport { Template } from \"./a2/template\";\nimport { ToolManager } from \"./a2/tool-manager\";\nimport {} from \"./a2/common\";\nimport {} from \"./a2/common\";\nimport { ArgumentNameGenerator } from \"./a2/introducer\";\nimport { ListExpander } from \"./a2/lists\";\nimport { executeStep, } from \"./a2/step-executor\";\nconst ASPECT_RATIOS = [\"9:16\", \"16:9\"];\nconst OUTPUT_NAME = \"generated_video\";\nexport { invoke as default, describe };\nasync function callVideoGen(prompt, imageContent, disablePromptRewrite, aspectRatio) {\n    const executionInputs = {};\n    const encodedPrompt = btoa(unescape(encodeURIComponent(prompt)));\n    executionInputs[\"text_instruction\"] = {\n        chunks: [\n            {\n                mimetype: \"text/plain\",\n                data: encodedPrompt,\n            },\n        ],\n    };\n    executionInputs[\"aspect_ratio_key\"] = {\n        chunks: [\n            {\n                mimetype: \"text/plain\",\n                data: btoa(aspectRatio),\n            },\n        ],\n    };\n    const inputParameters = [\"text_instruction\"];\n    if (imageContent) {\n        console.log(\"Image found, using i2v\");\n        let imageChunk;\n        if (isStoredData(imageContent)) {\n            imageChunk = toInlineReference(imageContent);\n        }\n        else {\n            imageChunk = toInlineData(imageContent);\n        }\n        if (!imageChunk || typeof imageChunk == \"string\") {\n            return toLLMContent(\"Image content did not have expected format\");\n        }\n        executionInputs[\"reference_image\"] = {\n            chunks: [\n                {\n                    mimetype: imageChunk.mimeType,\n                    data: imageChunk.data,\n                },\n            ],\n        };\n        inputParameters.push(\"reference_image\");\n    }\n    else {\n        console.log(\"No image found, using t2v\");\n    }\n    const body = {\n        planStep: {\n            stepName: \"GenerateVideo\",\n            modelApi: \"generate_video\",\n            inputParameters: inputParameters,\n            systemPrompt: \"\",\n            output: OUTPUT_NAME,\n            options: {\n                disablePromptRewrite,\n            },\n        },\n        execution_inputs: executionInputs,\n    };\n    // TODO(askerryryan): Remove when stable.\n    console.log(\"REQUEST:\");\n    console.log(body);\n    const response = await executeStep(body);\n    if (!ok(response)) {\n        return toLLMContent(\"Video generation failed: \" + response.$error);\n    }\n    if (!response.executionOutputs) {\n        return toLLMContent(\"Video generation failed to generate video\");\n    }\n    let returnVal;\n    for (let value of Object.values(response.executionOutputs)) {\n        const mimetype = value.chunks[0].mimetype;\n        if (mimetype.startsWith(\"video\")) {\n            if (mimetype.endsWith(\"/storedData\")) {\n                returnVal = toLLMContentStored(mimetype.replace(\"/storedData\", \"\"), value.chunks[0].data);\n            }\n            else {\n                returnVal = toLLMContentInline(mimetype, value.chunks[0].data);\n            }\n        }\n    }\n    if (!returnVal) {\n        return toLLMContent(\"Error: No video returned from backend\");\n    }\n    return returnVal;\n}\nasync function invoke({ context, instruction, \"p-disable-prompt-rewrite\": disablePromptRewrite, \"p-aspect-ratio\": aspectRatio, ...params }) {\n    context ??= [];\n    let instructionText = \"\";\n    if (instruction) {\n        instructionText = toText(instruction).trim();\n    }\n    if (!aspectRatio) {\n        aspectRatio = \"16:9\";\n    }\n    // 2) Substitute variables and magic image reference.\n    // Note: it is important that images are not subsituted in here as they will\n    // not be handled properly. At this point, only text variables should be left.\n    const template = new Template(toLLMContent(instructionText));\n    const toolManager = new ToolManager(new ArgumentNameGenerator());\n    const substituting = await template.substitute(params, async ({ path: url, instance }) => toolManager.addTool(url, instance));\n    if (!ok(substituting)) {\n        return substituting;\n    }\n    console.log(\"context\");\n    console.log(context);\n    console.log(\"instruction\");\n    console.log(instruction);\n    console.log(\"substituting\");\n    console.log(substituting);\n    const results = await new ListExpander(substituting, context).map(async (itemInstruction, itemContext) => {\n        // 1) Extract any image and text data from context (with history).\n        let imageContext = extractMediaData(itemContext);\n        const textContext = extractTextData(itemContext);\n        // 3) Extract image and text data from (non-history) references.\n        const refImages = extractMediaData([itemInstruction]);\n        const refText = extractTextData([itemInstruction]);\n        // 4) Combine with whatever data was extracted from context.\n        // Validate that we did not find any images, given this isn't supported yet.\n        imageContext = imageContext.concat(refImages);\n        if (imageContext.length > 1) {\n            return toLLMContent(`Video generation expects either a single text description, or text plus a single image. Got ${imageContext.length} images.`);\n        }\n        const combinedInstruction = toTextConcat(joinContent(toTextConcat(refText), textContext, false));\n        if (!combinedInstruction) {\n            return toLLMContent(\"A video instruction must be provided.\");\n        }\n        console.log(\"PROMPT: \", combinedInstruction);\n        // 2) Call backend to generate video.\n        const content = await callVideoGen(combinedInstruction, imageContext.at(0), disablePromptRewrite, aspectRatio);\n        return content;\n    });\n    if (!ok(results))\n        return results;\n    return { context: results };\n}\nasync function describe({ inputs: { instruction } }) {\n    const template = new Template(instruction);\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                instruction: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-preview\"],\n                    title: \"Instruction\",\n                    description: \"Instructions for how to render the video. Use @ to reference upstream steps.\",\n                    default: defaultLLMContent(),\n                },\n                \"p-disable-prompt-rewrite\": {\n                    type: \"boolean\",\n                    title: \"Disable prompt expansion\",\n                    behavior: [\"config\", \"hint-preview\"],\n                    description: \"By default, inputs and instructions can be automatically expanded into a higher quality video prompt. Check to disable this re-writing behavior.\",\n                },\n                \"p-aspect-ratio\": {\n                    type: \"string\",\n                    behavior: [\"hint-text\", \"config\"],\n                    title: \"Aspect Ratio\",\n                    enum: ASPECT_RATIOS,\n                    description: \"The aspect ratio of the generated video\",\n                    default: \"1:1\",\n                },\n                ...template.schemas(),\n            },\n            behavior: [\"at-wireable\"],\n            ...template.requireds(),\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"hint-multimodal\", \"main-port\"],\n                },\n            },\n            additionalProperties: false,\n        },\n        title: \"Make Video\",\n        metadata: {\n            icon: \"generative-video\",\n            tags: [\"quick-access\", \"generative\"],\n            order: 3,\n        },\n    };\n}\n",
+      "code": "/**\n * @fileoverview Generates video output using supplied context.\n */\nimport { ok, isStoredData, toTextConcat, joinContent, toLLMContent, toInlineReference, toLLMContentInline, toLLMContentStored, toText, toInlineData, extractMediaData, extractTextData, defaultLLMContent, } from \"./a2/utils\";\nimport { Template } from \"./a2/template\";\nimport { ToolManager } from \"./a2/tool-manager\";\nimport { ArgumentNameGenerator } from \"./a2/introducer\";\nimport { ListExpander } from \"./a2/lists\";\nimport { executeStep, } from \"./a2/step-executor\";\nconst ASPECT_RATIOS = [\"9:16\", \"16:9\"];\nconst OUTPUT_NAME = \"generated_video\";\nexport { invoke as default, describe };\nasync function callVideoGen(prompt, imageContent, disablePromptRewrite, aspectRatio) {\n    const executionInputs = {};\n    const encodedPrompt = btoa(unescape(encodeURIComponent(prompt)));\n    executionInputs[\"text_instruction\"] = {\n        chunks: [\n            {\n                mimetype: \"text/plain\",\n                data: encodedPrompt,\n            },\n        ],\n    };\n    executionInputs[\"aspect_ratio_key\"] = {\n        chunks: [\n            {\n                mimetype: \"text/plain\",\n                data: btoa(aspectRatio),\n            },\n        ],\n    };\n    const inputParameters = [\"text_instruction\"];\n    if (imageContent) {\n        console.log(\"Image found, using i2v\");\n        let imageChunk;\n        if (isStoredData(imageContent)) {\n            imageChunk = toInlineReference(imageContent);\n        }\n        else {\n            imageChunk = toInlineData(imageContent);\n        }\n        if (!imageChunk || typeof imageChunk == \"string\") {\n            return toLLMContent(\"Image content did not have expected format\");\n        }\n        executionInputs[\"reference_image\"] = {\n            chunks: [\n                {\n                    mimetype: imageChunk.mimeType,\n                    data: imageChunk.data,\n                },\n            ],\n        };\n        inputParameters.push(\"reference_image\");\n    }\n    else {\n        console.log(\"No image found, using t2v\");\n    }\n    const body = {\n        planStep: {\n            stepName: \"GenerateVideo\",\n            modelApi: \"generate_video\",\n            inputParameters: inputParameters,\n            systemPrompt: \"\",\n            output: OUTPUT_NAME,\n            options: {\n                disablePromptRewrite,\n            },\n        },\n        execution_inputs: executionInputs,\n    };\n    // TODO(askerryryan): Remove when stable.\n    console.log(\"REQUEST:\");\n    console.log(body);\n    const response = await executeStep(body);\n    if (!ok(response)) {\n        return toLLMContent(\"Video generation failed: \" + response.$error);\n    }\n    if (!response.executionOutputs) {\n        return toLLMContent(\"Video generation failed to generate video\");\n    }\n    let returnVal;\n    for (let value of Object.values(response.executionOutputs)) {\n        const mimetype = value.chunks[0].mimetype;\n        if (mimetype.startsWith(\"video\")) {\n            if (mimetype.endsWith(\"/storedData\")) {\n                returnVal = toLLMContentStored(mimetype.replace(\"/storedData\", \"\"), value.chunks[0].data);\n            }\n            else {\n                returnVal = toLLMContentInline(mimetype, value.chunks[0].data);\n            }\n        }\n    }\n    if (!returnVal) {\n        return toLLMContent(\"Error: No video returned from backend\");\n    }\n    return returnVal;\n}\nasync function invoke({ context, instruction, \"p-disable-prompt-rewrite\": disablePromptRewrite, \"p-aspect-ratio\": aspectRatio, ...params }) {\n    context ?? (context = []);\n    let instructionText = \"\";\n    if (instruction) {\n        instructionText = toText(instruction).trim();\n    }\n    if (!aspectRatio) {\n        aspectRatio = \"16:9\";\n    }\n    // 2) Substitute variables and magic image reference.\n    // Note: it is important that images are not subsituted in here as they will\n    // not be handled properly. At this point, only text variables should be left.\n    const template = new Template(toLLMContent(instructionText));\n    const toolManager = new ToolManager(new ArgumentNameGenerator());\n    const substituting = await template.substitute(params, async ({ path: url, instance }) => toolManager.addTool(url, instance));\n    if (!ok(substituting)) {\n        return substituting;\n    }\n    console.log(\"context\");\n    console.log(context);\n    console.log(\"instruction\");\n    console.log(instruction);\n    console.log(\"substituting\");\n    console.log(substituting);\n    const results = await new ListExpander(substituting, context).map(async (itemInstruction, itemContext) => {\n        // 1) Extract any image and text data from context (with history).\n        let imageContext = extractMediaData(itemContext);\n        const textContext = extractTextData(itemContext);\n        // 3) Extract image and text data from (non-history) references.\n        const refImages = extractMediaData([itemInstruction]);\n        const refText = extractTextData([itemInstruction]);\n        // 4) Combine with whatever data was extracted from context.\n        // Validate that we did not find any images, given this isn't supported yet.\n        imageContext = imageContext.concat(refImages);\n        if (imageContext.length > 1) {\n            return toLLMContent(`Video generation expects either a single text description, or text plus a single image. Got ${imageContext.length} images.`);\n        }\n        const combinedInstruction = toTextConcat(joinContent(toTextConcat(refText), textContext, false));\n        if (!combinedInstruction) {\n            return toLLMContent(\"A video instruction must be provided.\");\n        }\n        console.log(\"PROMPT: \", combinedInstruction);\n        // 2) Call backend to generate video.\n        const content = await callVideoGen(combinedInstruction, imageContext.at(0), disablePromptRewrite, aspectRatio);\n        return content;\n    });\n    if (!ok(results))\n        return results;\n    return { context: results };\n}\nasync function describe({ inputs: { instruction } }) {\n    const template = new Template(instruction);\n    return {\n        inputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context in\",\n                    behavior: [\"main-port\"],\n                },\n                instruction: {\n                    type: \"object\",\n                    behavior: [\"llm-content\", \"config\", \"hint-preview\"],\n                    title: \"Instruction\",\n                    description: \"Instructions for how to render the video. Use @ to reference upstream steps.\",\n                    default: defaultLLMContent(),\n                },\n                \"p-disable-prompt-rewrite\": {\n                    type: \"boolean\",\n                    title: \"Disable prompt expansion\",\n                    behavior: [\"config\", \"hint-preview\"],\n                    description: \"By default, inputs and instructions can be automatically expanded into a higher quality video prompt. Check to disable this re-writing behavior.\",\n                },\n                \"p-aspect-ratio\": {\n                    type: \"string\",\n                    behavior: [\"hint-text\", \"config\"],\n                    title: \"Aspect Ratio\",\n                    enum: ASPECT_RATIOS,\n                    description: \"The aspect ratio of the generated video\",\n                    default: \"1:1\",\n                },\n                ...template.schemas(),\n            },\n            behavior: [\"at-wireable\"],\n            ...template.requireds(),\n        },\n        outputSchema: {\n            type: \"object\",\n            properties: {\n                context: {\n                    type: \"array\",\n                    items: { type: \"object\", behavior: [\"llm-content\"] },\n                    title: \"Context out\",\n                    behavior: [\"hint-multimodal\", \"main-port\"],\n                },\n            },\n            additionalProperties: false,\n        },\n        title: \"Make Video\",\n        metadata: {\n            icon: \"generative-video\",\n            tags: [\"quick-access\", \"generative\"],\n            order: 3,\n        },\n    };\n}\n",
       "metadata": {
         "title": "main",
         "source": {
diff --git a/packages/a2/package.json b/packages/a2/package.json
index 43e6fbc841f..2e8dd7d92ca 100644
--- a/packages/a2/package.json
+++ b/packages/a2/package.json
@@ -6,7 +6,8 @@
   "type": "module",
   "scripts": {
     "push": "tsx scripts/push.ts",
-    "build": "wireit"
+    "build": "wireit",
+    "sync-boards": "tsx ./scripts/sync-boards.ts"
   },
   "wireit": {
     "build": {
@@ -56,10 +57,12 @@
   "homepage": "https://github.com/breadboard-ai/breadboard#readme",
   "devDependencies": {
     "@google-cloud/firestore": "^7.11.0",
+    "@types/glob": "^8.1.0",
     "@types/node": "^22.0.0",
     "@typescript-eslint/eslint-plugin": "^7.18.0",
     "@typescript-eslint/parser": "^7.18.0",
     "dotenv": "^16.4.7",
+    "glob": "^11.0.2",
     "tsx": "^4.19.3",
     "typescript": "^5.8.3"
   },
diff --git a/packages/a2/scripts/sync-boards.ts b/packages/a2/scripts/sync-boards.ts
new file mode 100644
index 00000000000..553148e8069
--- /dev/null
+++ b/packages/a2/scripts/sync-boards.ts
@@ -0,0 +1,336 @@
+// Synchronizes TypeScript source files with corresponding `.bgl.json` board files.
+//
+// It's designed to:
+//   1. Only process `.bgl.json` files that ALREADY EXIST. It does not create new ones.
+//   2. For each existing board file, find its corresponding TypeScript source directory.
+//   3. For each TypeScript file found, if a module entry with the same key ALREADY EXISTS
+//      in the board's JSON, it will update ONLY two specific fields:
+//      - `modules[moduleKey].code` (with transpiled JavaScript)
+//      - `modules[moduleKey].metadata.source.code` (with raw TypeScript content)
+//   4. If a TypeScript file is found but its corresponding module key does not exist in the
+//      JSON, that TS file is SKIPPED, and no new module entry is created.
+//   5. All other data within the JSON (other module metadata, nodes, edges, board-level
+//      metadata, imports, exports, etc.) is PRESERVED.
+//   6. The script saves a board file back only if actual changes to the targeted fields
+//      were made.
+import * as fs from "node:fs";
+import * as path from "node:path";
+import * as ts from "typescript";
+import { globSync } from "glob";
+import {
+  GraphDescriptor,
+  Module,
+  ModuleMetadata as GraphModuleMetadata,
+  ModuleCode,
+  ModuleLanguage,
+} from "@breadboard-ai/types";
+
+// --- Script-level Constants and Initialized Compiler Options ---
+const SCRIPT_PROJECT_ROOT = process.cwd();
+let SCRIPT_COMPILER_OPTIONS: ts.CompilerOptions;
+
+function initializeCompilerOptions(): ts.CompilerOptions {
+  const defaultOptions: ts.CompilerOptions = {
+    module: ts.ModuleKind.ESNext,
+    target: ts.ScriptTarget.ESNext,
+    esModuleInterop: true,
+    sourceMap: false, // Script specifically wants no source maps for embedded code
+    // Add any other non-negotiable defaults for the script here
+  };
+
+  const tsConfigPath = ts.findConfigFile(
+    SCRIPT_PROJECT_ROOT,
+    ts.sys.fileExists,
+    "tsconfig.json"
+  );
+
+  if (!tsConfigPath) {
+    console.warn(
+      "Warning: tsconfig.json not found at project root. Using default transpilation options for the script."
+    );
+    return defaultOptions;
+  }
+
+  console.log(`Attempting to load compiler options from: ${tsConfigPath}`);
+  const configFile = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
+
+  if (configFile.error) {
+    console.warn(
+      `Warning: Error reading tsconfig.json: ${ts.flattenDiagnosticMessageText(
+        configFile.error.messageText,
+        "\n"
+      )}. Using default transpilation options for the script.`
+    );
+    return defaultOptions;
+  }
+
+  const parsedCmd = ts.parseJsonConfigFileContent(
+    configFile.config, // The raw JSON object from tsconfig.json
+    ts.sys, // System interface for file operations
+    path.dirname(tsConfigPath) // Base path to resolve 'extends' and other relative paths
+  );
+
+  if (parsedCmd.errors.length > 0) {
+    console.warn(
+      "Warning: Errors parsing tsconfig.json. This might be due to issues in the tsconfig itself or referenced files (e.g., 'extends'). Using default transpilation options for the script."
+    );
+    parsedCmd.errors.forEach((err) =>
+      console.warn(
+        `  - ${ts.flattenDiagnosticMessageText(err.messageText, "\n")}`
+      )
+    );
+    return defaultOptions;
+  }
+
+  console.log("Successfully loaded and parsed tsconfig.json compiler options.");
+
+  // Merge options:
+  // 1. Start with script's essential defaults.
+  // 2. Override with options from tsconfig.json.
+  // 3. Apply any final, non-negotiable script overrides.
+  return {
+    ...defaultOptions, // Script defaults
+    ...parsedCmd.options, // Options from tsconfig.json (parsed and resolved)
+    sourceMap: false, // Ensure sourceMap is false, overriding tsconfig if it sets it to true
+    // listFiles: undefined, // Avoid options not relevant to transpileModule if they cause issues
+    // noEmit: undefined,    // Ensure these are not set to true if they come from tsconfig
+    // declaration: undefined,
+    // declarationMap: undefined,
+    // emitDeclarationOnly: undefined,
+  };
+}
+
+SCRIPT_COMPILER_OPTIONS = initializeCompilerOptions();
+
+// --- Main Processing Logic ---
+
+/**
+ * Processes a single .bgl.json board file.
+ * - Skips if the board file doesn't exist.
+ * - Reads the board JSON.
+ * - Finds corresponding TS files in `bgl/src/BOARD_NAME/`.
+ * - For each TS file matching an existing module key in the JSON:
+ * - Updates `module.code` and `module.metadata.source.code`.
+ * - Preserves all other data.
+ * - Writes back to the file ONLY if modifications were made.
+ * @param boardFilePath Absolute path to the .bgl.json file.
+ */
+async function processBoard(boardFilePath: string): Promise<void> {
+  const boardFileName = path.basename(boardFilePath);
+  const boardName = path.basename(boardFileName, ".bgl.json");
+
+  // Edge Case Handling: Script only operates on .bgl.json files that already exist.
+  // It will not create a new .bgl.json file if one isn't found.
+  if (!fs.existsSync(boardFilePath)) {
+    console.log(
+      `[${boardName}] Skipping: Board file ${boardFileName} does not exist at ${boardFilePath}.`
+    );
+    return;
+  }
+
+  console.log(`[${boardName}] Processing existing board file...`);
+
+  let boardJsonData: GraphDescriptor;
+  try {
+    // Read the existing JSON content.
+    boardJsonData = JSON.parse(
+      fs.readFileSync(boardFilePath, "utf-8")
+    ) as GraphDescriptor;
+  } catch (error: any) {
+    // Edge Case Handling: If JSON is malformed, log error and skip this board.
+    console.error(
+      `[${boardName}] Error parsing existing JSON: ${error.message}. Please check its format. Skipping this board.`
+    );
+    return;
+  }
+
+  // Define paths relative to the project root where the script is expected to run.
+  const PROJECT_ROOT = process.cwd();
+  const BGL_ROOT_DIR = path.join(PROJECT_ROOT, "bgl");
+  const SRC_ROOT_DIR = path.join(BGL_ROOT_DIR, "src");
+
+  // Convention: TypeScript source files for a board are located in a subdirectory
+  // named after the board within `bgl/src/`.
+  const tsSourceDir = path.join(SRC_ROOT_DIR, boardName);
+  let boardWasModified = false; // Flag to track if any actual changes are made to this board.
+
+  // Check if the corresponding TypeScript source directory exists.
+  if (fs.existsSync(tsSourceDir) && fs.lstatSync(tsSourceDir).isDirectory()) {
+    // Find all .ts files in the board's source directory.
+    if (!boardJsonData.modules) {
+      console.log(
+        `  [${boardName}] No 'modules' object found in the JSON. Skipping TypeScript module synchronization for this board.`
+      );
+    } else {
+      const tsFiles = globSync("*.ts", { cwd: tsSourceDir, absolute: false });
+
+      if (tsFiles.length === 0) {
+        console.log(
+          `  [${boardName}] No TypeScript files found in ${tsSourceDir}.`
+        );
+      }
+
+      for (const tsFile of tsFiles) {
+        // tsFile is relative path like 'main.ts'
+        const moduleKey = path.basename(tsFile, ".ts"); // e.g., 'main' from 'main.ts'
+        const fullTsFilePath = path.join(tsSourceDir, tsFile);
+
+        // --- Core Logic: Only update if moduleKey already exists in JSON ---
+        // If a .ts file exists but there's no corresponding entry in the .bgl.json's 'modules'
+        // object, this .ts file is SKIPPED. No new module entry will be created in the JSON.
+        // This ensures we only update targeted fields of PRE-EXISTING modules.
+        if (!boardJsonData.modules[moduleKey]) {
+          console.log(
+            `  [${boardName}] Skipping TS file "${tsFile}": Module key "${moduleKey}" not found in existing JSON. No new module will be created.`
+          );
+          continue;
+        }
+
+        console.log(
+          `  [${boardName}] Updating existing module "${moduleKey}" from "${tsFile}"...`
+        );
+        const moduleEntry: Module = boardJsonData.modules[moduleKey]; // Type is Module from graph-descriptor.ts
+        const tsContent = fs.readFileSync(fullTsFilePath, "utf-8"); // Read raw TS content.
+
+        const transpileOutput = ts.transpileModule(tsContent, {
+          compilerOptions: SCRIPT_COMPILER_OPTIONS,
+        });
+
+        // Edge Case Handling: Log TypeScript transpilation errors/warnings if any occur.
+        // The script will still proceed with the (potentially flawed) transpiled output.
+        if (
+          transpileOutput.diagnostics &&
+          transpileOutput.diagnostics.length > 0
+        ) {
+          console.warn(
+            `    [${boardName}] TypeScript transpilation issues for "${fullTsFilePath}":`
+          );
+          transpileOutput.diagnostics.forEach((diagnostic) => {
+            const message = ts.flattenDiagnosticMessageText(
+              diagnostic.messageText,
+              "\n"
+            );
+            if (diagnostic.file && typeof diagnostic.start === "number") {
+              // Check if start is a number
+              const { line, character } =
+                diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
+              console.warn(
+                `      Error ${diagnostic.code} at ${line + 1},${character + 1}: ${message}`
+              );
+            } else {
+              console.warn(`      Error ${diagnostic.code}: ${message}`);
+            }
+          });
+        }
+        const jsCode: ModuleCode = transpileOutput.outputText; // Get the transpiled JavaScript code.
+
+        // --- Perform Targeted Updates ---
+        // 1. Update the `code` field (transpiled JavaScript).
+        moduleEntry.code = jsCode;
+
+        // 2. Update `metadata.source.code` (raw TypeScript).
+        //    Ensure the parent `metadata` and `metadata.source` objects exist.
+        //    Any other pre-existing fields within `metadata` or `metadata.source` are preserved.
+
+        // Ensure metadata object exists on the module entry
+        if (!moduleEntry.metadata) {
+          console.log(
+            `    [${boardName}] Initializing 'metadata' for module "${moduleKey}" as it was missing.`
+          );
+          moduleEntry.metadata = {} as GraphModuleMetadata;
+        }
+
+        // Ensure metadata.source object exists and update/set its properties
+        if (!moduleEntry.metadata.source) {
+          console.log(
+            `    [${boardName}] Initializing 'metadata.source' for module "${moduleKey}".`
+          );
+          moduleEntry.metadata.source = {
+            code: tsContent,
+            language: "typescript" as ModuleLanguage,
+          };
+        } else {
+          moduleEntry.metadata.source.code = tsContent;
+          // Ensure language is set, in case it was different or missing
+          moduleEntry.metadata.source.language = "typescript" as ModuleLanguage;
+        }
+        boardWasModified = true; // Mark that this board has been changed.
+      }
+    }
+  } else {
+    // Edge Case Handling: No TypeScript source directory found for this board.
+    // No modules will be updated from TS files. The JSON is preserved as is.
+    console.log(
+      `  [${boardName}] No source directory found at "${tsSourceDir}". No modules updated from TS sources.`
+    );
+  }
+
+  // Workflow: Only write back to the .bgl.json file if actual modifications
+  // to the targeted `code` or `metadata.source.code` fields were made.
+  if (boardWasModified) {
+    try {
+      fs.writeFileSync(
+        boardFilePath,
+        JSON.stringify(boardJsonData, null, 2),
+        "utf-8"
+      );
+      console.log(
+        `[${boardName}] Successfully synchronized and saved changes.`
+      );
+    } catch (error: any) {
+      // Edge Case Handling: Error during file writing (e.g., permissions, disk full).
+      console.error(
+        `[${boardName}] Error writing updated JSON: ${error.message}`
+      );
+    }
+  } else {
+    console.log(`[${boardName}] No relevant changes to save.`);
+  }
+}
+
+/**
+ * Main function to orchestrate the synchronization of all boards.
+ * - Finds all .bgl.json files in the BGL_ROOT_DIR.
+ * - Calls processBoard for each found file.
+ */
+async function syncAllBoards(): Promise<void> {
+  console.log(
+    "Starting board synchronization (TypeScript version, targeted updates)..."
+  );
+  const PROJECT_ROOT = process.cwd();
+  const BGL_ROOT_DIR = path.join(PROJECT_ROOT, "bgl"); // e.g., './bgl'
+
+  // Discover all .bgl.json files in the specified root directory.
+  const boardFilePaths = globSync("*.bgl.json", {
+    cwd: BGL_ROOT_DIR,
+    absolute: true,
+  });
+
+  // Edge Case Handling: No .bgl.json files found.
+  if (boardFilePaths.length === 0) {
+    console.warn(
+      `No *.bgl.json files found in "${BGL_ROOT_DIR}". Nothing to sync.`
+    );
+    return;
+  }
+
+  console.log(`Found ${boardFilePaths.length} board file(s) to process.`);
+
+  // Process each board file sequentially.
+  // Using a loop with await ensures one board finishes before the next starts,
+  // which is good for clearer logs and avoiding race conditions if any existed.
+  for (const boardFilePath of boardFilePaths) {
+    await processBoard(boardFilePath);
+  }
+  console.log("All boards synchronization process finished.");
+}
+
+// Execute the main synchronization function.
+syncAllBoards().catch((error) => {
+  // Catch any unhandled errors from the async operations.
+  console.error(
+    "An unhandled error occurred during the synchronization process:",
+    error
+  );
+  process.exit(1); // Exit with a non-zero code to indicate failure.
+});


Reviews:
user: login: dglazkov
body: This is great, modulo a couple of nits. Thank you for doing the grungy bits ❤️ 
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-06T19:47:59Z
user: login: jameszhao00
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T02:33:15Z
user: login: jameszhao00
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T02:33:19Z
user: login: jameszhao00
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T02:33:49Z
user: login: jameszhao00
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T02:34:37Z
user: login: dglazkov
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T02:47:54Z
user: login: jameszhao00
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T04:05:57Z

Review comments:
diff_hunk: @@ -6,7 +6,8 @@
   "type": "module",
   "scripts": {
     "push": "tsx scripts/push.ts",
-    "build": "wireit"
+    "build": "wireit",
+    "sync-boards": "ts-node ./scripts/sync-boards.ts"
path: packages/a2/package.json
user: login: dglazkov
body: Let' use tsx, which we use a lot. I'd love to get rid of ts-node as deps altogether, and I think there's only one old bit of code that uses it.
created_at: 2025-05-06T19:42:40Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,281 @@
+// Synchronizes TypeScript source files with corresponding `.bgl.json` board files.
+//
+// It's designed to:
+//   1. Only process `.bgl.json` files that ALREADY EXIST. It does not create new ones.
+//   2. For each existing board file, find its corresponding TypeScript source directory.
+//   3. For each TypeScript file found, if a module entry with the same key ALREADY EXISTS
+//      in the board's JSON, it will update ONLY two specific fields:
+//      - `modules[moduleKey].code` (with transpiled JavaScript)
+//      - `modules[moduleKey].metadata.source.code` (with raw TypeScript content)
+//   4. If a TypeScript file is found but its corresponding module key does not exist in the
+//      JSON, that TS file is SKIPPED, and no new module entry is created.
+//   5. All other data within the JSON (other module metadata, nodes, edges, board-level
+//      metadata, imports, exports, etc.) is PRESERVED.
+//   6. The script saves a board file back only if actual changes to the targeted fields
+//      were made.
+
+import * as fs from 'node:fs';
+import * as path from 'node:path';
+import * as ts from 'typescript';
+import { globSync } from 'glob';
+
+// --- Type Definitions for Board JSON Structure ---
+// These interfaces describe the expected (or at least parts of) structure of the .bgl.json files.
+// They help ensure the script interacts with the JSON data in a type-safe manner.
+
+interface ModuleSource {
+  code: string;       // Raw TypeScript content will be placed here.
+  language: string;   // Expected to be "typescript"; will be set if `source` object is created.
+  [key: string]: any; // Allows for other, unspecified fields within the source object.
+}
+
+interface ModuleMetadataVisual {
+  x?: number;
+  y?: number;
+  collapsed?: string;
+  outputHeight?: number;
+  [key: string]: any;
+}
+
+interface ModuleCommentMetadata {
+    title?: string;
+    visual?: ModuleMetadataVisual;
+    [key: string]: any;
+}
+
+interface ModuleComment {
+    id: string;
+    text: string;
+    metadata: ModuleCommentMetadata;
+}
+
+interface ModuleMetadata {
+  title?: string;         // This field will be PRESERVED if it exists.
+  description?: string;   // This field will be PRESERVED if it exists.
+  source?: ModuleSource;  // The `.code` sub-field here will be UPDATED.
+  runnable?: boolean;     // This field will be PRESERVED if it exists.
+  [key: string]: any;     // Allows for other, unspecified fields within module metadata.
+}
+
+interface ModuleEntry {
+  code: string;           // This field (transpiled JS) will be UPDATED.
+  metadata?: ModuleMetadata;// Its `.source.code` sub-field will be UPDATED.
+  [key: string]: any;     // Allows for other, unspecified fields within a module entry.
+}
+
+interface BoardMetadata {
+  comments?: ModuleComment[];
+  visual?: { [key: string]: any };
+  tags?: string[];
+  [key: string]: any;
+}
+
+interface BoardImport {
+    url: string;
+    [key: string]: any;
+}
+
+interface BoardJSON {
path: packages/a2/scripts/sync-boards.ts
user: login: dglazkov
body: Here and elsewhere, let's use `GraphDescriptor` from "@breaboard-ai/types", so that we don't have a weird version skews.
created_at: 2025-05-06T19:44:37Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,281 @@
+// Synchronizes TypeScript source files with corresponding `.bgl.json` board files.
+//
+// It's designed to:
+//   1. Only process `.bgl.json` files that ALREADY EXIST. It does not create new ones.
+//   2. For each existing board file, find its corresponding TypeScript source directory.
+//   3. For each TypeScript file found, if a module entry with the same key ALREADY EXISTS
+//      in the board's JSON, it will update ONLY two specific fields:
+//      - `modules[moduleKey].code` (with transpiled JavaScript)
+//      - `modules[moduleKey].metadata.source.code` (with raw TypeScript content)
+//   4. If a TypeScript file is found but its corresponding module key does not exist in the
+//      JSON, that TS file is SKIPPED, and no new module entry is created.
+//   5. All other data within the JSON (other module metadata, nodes, edges, board-level
+//      metadata, imports, exports, etc.) is PRESERVED.
+//   6. The script saves a board file back only if actual changes to the targeted fields
+//      were made.
+
+import * as fs from 'node:fs';
+import * as path from 'node:path';
+import * as ts from 'typescript';
+import { globSync } from 'glob';
+
+// --- Type Definitions for Board JSON Structure ---
+// These interfaces describe the expected (or at least parts of) structure of the .bgl.json files.
+// They help ensure the script interacts with the JSON data in a type-safe manner.
+
+interface ModuleSource {
+  code: string;       // Raw TypeScript content will be placed here.
+  language: string;   // Expected to be "typescript"; will be set if `source` object is created.
+  [key: string]: any; // Allows for other, unspecified fields within the source object.
+}
+
+interface ModuleMetadataVisual {
+  x?: number;
+  y?: number;
+  collapsed?: string;
+  outputHeight?: number;
+  [key: string]: any;
+}
+
+interface ModuleCommentMetadata {
+    title?: string;
+    visual?: ModuleMetadataVisual;
+    [key: string]: any;
+}
+
+interface ModuleComment {
+    id: string;
+    text: string;
+    metadata: ModuleCommentMetadata;
+}
+
+interface ModuleMetadata {
+  title?: string;         // This field will be PRESERVED if it exists.
+  description?: string;   // This field will be PRESERVED if it exists.
+  source?: ModuleSource;  // The `.code` sub-field here will be UPDATED.
+  runnable?: boolean;     // This field will be PRESERVED if it exists.
+  [key: string]: any;     // Allows for other, unspecified fields within module metadata.
+}
+
+interface ModuleEntry {
+  code: string;           // This field (transpiled JS) will be UPDATED.
+  metadata?: ModuleMetadata;// Its `.source.code` sub-field will be UPDATED.
+  [key: string]: any;     // Allows for other, unspecified fields within a module entry.
+}
+
+interface BoardMetadata {
+  comments?: ModuleComment[];
+  visual?: { [key: string]: any };
+  tags?: string[];
+  [key: string]: any;
+}
+
+interface BoardImport {
+    url: string;
+    [key: string]: any;
+}
+
+interface BoardJSON {
+  title: string;
+  description: string;
+  version: string;
+  nodes: any[];           // Preserved as-is.
+  edges: any[];           // Preserved as-is.
+  metadata: BoardMetadata;// Preserved as-is.
+  modules: {
+    [moduleKey: string]: ModuleEntry; // Entries here are candidates for targeted updates.
+  };
+  imports: {
+    [importKey: string]: BoardImport; // Preserved as-is.
+  };
+  exports: string[];      // Preserved as-is.
+  [key: string]: any;     // Allows for other top-level fields in the JSON.
+}
+
+// --- Main Processing Logic ---
+
+/**
+ * Processes a single .bgl.json board file.
+ * - Skips if the board file doesn't exist.
+ * - Reads the board JSON.
+ * - Finds corresponding TS files in `bgl/src/BOARD_NAME/`.
+ * - For each TS file matching an existing module key in the JSON:
+ * - Updates `module.code` and `module.metadata.source.code`.
+ * - Preserves all other data.
+ * - Writes back to the file ONLY if modifications were made.
+ * @param boardFilePath Absolute path to the .bgl.json file.
+ */
+async function processBoard(boardFilePath: string): Promise<void> {
+  const boardFileName = path.basename(boardFilePath);
+  const boardName = path.basename(boardFileName, '.bgl.json');
+
+  // Edge Case Handling: Script only operates on .bgl.json files that already exist.
+  // It will not create a new .bgl.json file if one isn't found.
+  if (!fs.existsSync(boardFilePath)) {
+    console.log(`[${boardName}] Skipping: Board file ${boardFileName} does not exist at ${boardFilePath}.`);
+    return;
+  }
+
+  console.log(`[${boardName}] Processing existing board file...`);
+
+  let boardJsonData: BoardJSON;
+  try {
+    // Read the existing JSON content.
+    boardJsonData = JSON.parse(fs.readFileSync(boardFilePath, 'utf-8')) as BoardJSON;
+  } catch (error: any) {
+    // Edge Case Handling: If JSON is malformed, log error and skip this board.
+    console.error(`[${boardName}] Error parsing existing JSON: ${error.message}. Please check its format. Skipping this board.`);
+    return;
+  }
+
+  // Ensure 'modules' object exists on the loaded JSON structure.
+  // This is a safeguard, though well-formed boards should have it.
+  boardJsonData.modules = boardJsonData.modules || {};
path: packages/a2/scripts/sync-boards.ts
user: login: dglazkov
body: This might not always be true, but will definitely be weird in A2.
created_at: 2025-05-06T19:45:31Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,281 @@
+// Synchronizes TypeScript source files with corresponding `.bgl.json` board files.
+//
+// It's designed to:
+//   1. Only process `.bgl.json` files that ALREADY EXIST. It does not create new ones.
+//   2. For each existing board file, find its corresponding TypeScript source directory.
+//   3. For each TypeScript file found, if a module entry with the same key ALREADY EXISTS
+//      in the board's JSON, it will update ONLY two specific fields:
+//      - `modules[moduleKey].code` (with transpiled JavaScript)
+//      - `modules[moduleKey].metadata.source.code` (with raw TypeScript content)
+//   4. If a TypeScript file is found but its corresponding module key does not exist in the
+//      JSON, that TS file is SKIPPED, and no new module entry is created.
+//   5. All other data within the JSON (other module metadata, nodes, edges, board-level
+//      metadata, imports, exports, etc.) is PRESERVED.
+//   6. The script saves a board file back only if actual changes to the targeted fields
+//      were made.
+
+import * as fs from 'node:fs';
+import * as path from 'node:path';
+import * as ts from 'typescript';
+import { globSync } from 'glob';
+
+// --- Type Definitions for Board JSON Structure ---
+// These interfaces describe the expected (or at least parts of) structure of the .bgl.json files.
+// They help ensure the script interacts with the JSON data in a type-safe manner.
+
+interface ModuleSource {
+  code: string;       // Raw TypeScript content will be placed here.
+  language: string;   // Expected to be "typescript"; will be set if `source` object is created.
+  [key: string]: any; // Allows for other, unspecified fields within the source object.
+}
+
+interface ModuleMetadataVisual {
+  x?: number;
+  y?: number;
+  collapsed?: string;
+  outputHeight?: number;
+  [key: string]: any;
+}
+
+interface ModuleCommentMetadata {
+    title?: string;
+    visual?: ModuleMetadataVisual;
+    [key: string]: any;
+}
+
+interface ModuleComment {
+    id: string;
+    text: string;
+    metadata: ModuleCommentMetadata;
+}
+
+interface ModuleMetadata {
+  title?: string;         // This field will be PRESERVED if it exists.
+  description?: string;   // This field will be PRESERVED if it exists.
+  source?: ModuleSource;  // The `.code` sub-field here will be UPDATED.
+  runnable?: boolean;     // This field will be PRESERVED if it exists.
+  [key: string]: any;     // Allows for other, unspecified fields within module metadata.
+}
+
+interface ModuleEntry {
+  code: string;           // This field (transpiled JS) will be UPDATED.
+  metadata?: ModuleMetadata;// Its `.source.code` sub-field will be UPDATED.
+  [key: string]: any;     // Allows for other, unspecified fields within a module entry.
+}
+
+interface BoardMetadata {
+  comments?: ModuleComment[];
+  visual?: { [key: string]: any };
+  tags?: string[];
+  [key: string]: any;
+}
+
+interface BoardImport {
+    url: string;
+    [key: string]: any;
+}
+
+interface BoardJSON {
+  title: string;
+  description: string;
+  version: string;
+  nodes: any[];           // Preserved as-is.
+  edges: any[];           // Preserved as-is.
+  metadata: BoardMetadata;// Preserved as-is.
+  modules: {
+    [moduleKey: string]: ModuleEntry; // Entries here are candidates for targeted updates.
+  };
+  imports: {
+    [importKey: string]: BoardImport; // Preserved as-is.
+  };
+  exports: string[];      // Preserved as-is.
+  [key: string]: any;     // Allows for other top-level fields in the JSON.
+}
+
+// --- Main Processing Logic ---
+
+/**
+ * Processes a single .bgl.json board file.
+ * - Skips if the board file doesn't exist.
+ * - Reads the board JSON.
+ * - Finds corresponding TS files in `bgl/src/BOARD_NAME/`.
+ * - For each TS file matching an existing module key in the JSON:
+ * - Updates `module.code` and `module.metadata.source.code`.
+ * - Preserves all other data.
+ * - Writes back to the file ONLY if modifications were made.
+ * @param boardFilePath Absolute path to the .bgl.json file.
+ */
+async function processBoard(boardFilePath: string): Promise<void> {
+  const boardFileName = path.basename(boardFilePath);
+  const boardName = path.basename(boardFileName, '.bgl.json');
+
+  // Edge Case Handling: Script only operates on .bgl.json files that already exist.
+  // It will not create a new .bgl.json file if one isn't found.
+  if (!fs.existsSync(boardFilePath)) {
+    console.log(`[${boardName}] Skipping: Board file ${boardFileName} does not exist at ${boardFilePath}.`);
+    return;
+  }
+
+  console.log(`[${boardName}] Processing existing board file...`);
+
+  let boardJsonData: BoardJSON;
+  try {
+    // Read the existing JSON content.
+    boardJsonData = JSON.parse(fs.readFileSync(boardFilePath, 'utf-8')) as BoardJSON;
+  } catch (error: any) {
+    // Edge Case Handling: If JSON is malformed, log error and skip this board.
+    console.error(`[${boardName}] Error parsing existing JSON: ${error.message}. Please check its format. Skipping this board.`);
+    return;
+  }
+
+  // Ensure 'modules' object exists on the loaded JSON structure.
+  // This is a safeguard, though well-formed boards should have it.
+  boardJsonData.modules = boardJsonData.modules || {};
+
+  // Define paths relative to the project root where the script is expected to run.
+  const PROJECT_ROOT = process.cwd();
+  const BGL_ROOT_DIR = path.join(PROJECT_ROOT, 'bgl');
+  const SRC_ROOT_DIR = path.join(BGL_ROOT_DIR, 'src');
+
+  // Convention: TypeScript source files for a board are located in a subdirectory
+  // named after the board within `bgl/src/`.
+  const tsSourceDir = path.join(SRC_ROOT_DIR, boardName);
+  let boardWasModified = false; // Flag to track if any actual changes are made to this board.
+
+  // Check if the corresponding TypeScript source directory exists.
+  if (fs.existsSync(tsSourceDir) && fs.lstatSync(tsSourceDir).isDirectory()) {
+    // Find all .ts files in the board's source directory.
+    const tsFiles = globSync('*.ts', { cwd: tsSourceDir, absolute: false });
+
+    if (tsFiles.length === 0) {
+      console.log(`  [${boardName}] No TypeScript files found in ${tsSourceDir}.`);
+    }
+
+    for (const tsFile of tsFiles) { // tsFile is relative path like 'main.ts'
+      const moduleKey = path.basename(tsFile, '.ts'); // e.g., 'main' from 'main.ts'
+      const fullTsFilePath = path.join(tsSourceDir, tsFile);
+
+      // --- Core Logic: Only update if moduleKey already exists in JSON ---
+      // If a .ts file exists but there's no corresponding entry in the .bgl.json's 'modules'
+      // object, this .ts file is SKIPPED. No new module entry will be created in the JSON.
+      // This ensures we only update targeted fields of PRE-EXISTING modules.
+      if (!boardJsonData.modules[moduleKey]) {
+        console.log(`  [${boardName}] Skipping TS file "${tsFile}": Module key "${moduleKey}" not found in existing JSON. No new module will be created.`);
+        continue;
+      }
+
+      console.log(`  [${boardName}] Updating existing module "${moduleKey}" from "${tsFile}"...`);
+      const moduleEntry = boardJsonData.modules[moduleKey]; // Get reference to existing module entry.
+      const tsContent = fs.readFileSync(fullTsFilePath, 'utf-8'); // Read raw TS content.
+
+      // Configure TypeScript transpilation.
+      // IMPORTANT: Align these options with your project's tsconfig.json for consistency,
+      // or enhance script to load compiler options from tsconfig.json.
+      const compilerOptions: ts.CompilerOptions = {
path: packages/a2/scripts/sync-boards.ts
user: login: dglazkov
body: This is hacky -- as the comment suggests, we're much better off using the tsconfig.json.
created_at: 2025-05-06T19:46:34Z
author_association: COLLABORATOR
diff_hunk: @@ -6,7 +6,8 @@
   "type": "module",
   "scripts": {
     "push": "tsx scripts/push.ts",
-    "build": "wireit"
+    "build": "wireit",
+    "sync-boards": "ts-node ./scripts/sync-boards.ts"
path: packages/a2/package.json
user: login: jameszhao00
body: Done
created_at: 2025-05-07T02:33:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,281 @@
+// Synchronizes TypeScript source files with corresponding `.bgl.json` board files.
+//
+// It's designed to:
+//   1. Only process `.bgl.json` files that ALREADY EXIST. It does not create new ones.
+//   2. For each existing board file, find its corresponding TypeScript source directory.
+//   3. For each TypeScript file found, if a module entry with the same key ALREADY EXISTS
+//      in the board's JSON, it will update ONLY two specific fields:
+//      - `modules[moduleKey].code` (with transpiled JavaScript)
+//      - `modules[moduleKey].metadata.source.code` (with raw TypeScript content)
+//   4. If a TypeScript file is found but its corresponding module key does not exist in the
+//      JSON, that TS file is SKIPPED, and no new module entry is created.
+//   5. All other data within the JSON (other module metadata, nodes, edges, board-level
+//      metadata, imports, exports, etc.) is PRESERVED.
+//   6. The script saves a board file back only if actual changes to the targeted fields
+//      were made.
+
+import * as fs from 'node:fs';
+import * as path from 'node:path';
+import * as ts from 'typescript';
+import { globSync } from 'glob';
+
+// --- Type Definitions for Board JSON Structure ---
+// These interfaces describe the expected (or at least parts of) structure of the .bgl.json files.
+// They help ensure the script interacts with the JSON data in a type-safe manner.
+
+interface ModuleSource {
+  code: string;       // Raw TypeScript content will be placed here.
+  language: string;   // Expected to be "typescript"; will be set if `source` object is created.
+  [key: string]: any; // Allows for other, unspecified fields within the source object.
+}
+
+interface ModuleMetadataVisual {
+  x?: number;
+  y?: number;
+  collapsed?: string;
+  outputHeight?: number;
+  [key: string]: any;
+}
+
+interface ModuleCommentMetadata {
+    title?: string;
+    visual?: ModuleMetadataVisual;
+    [key: string]: any;
+}
+
+interface ModuleComment {
+    id: string;
+    text: string;
+    metadata: ModuleCommentMetadata;
+}
+
+interface ModuleMetadata {
+  title?: string;         // This field will be PRESERVED if it exists.
+  description?: string;   // This field will be PRESERVED if it exists.
+  source?: ModuleSource;  // The `.code` sub-field here will be UPDATED.
+  runnable?: boolean;     // This field will be PRESERVED if it exists.
+  [key: string]: any;     // Allows for other, unspecified fields within module metadata.
+}
+
+interface ModuleEntry {
+  code: string;           // This field (transpiled JS) will be UPDATED.
+  metadata?: ModuleMetadata;// Its `.source.code` sub-field will be UPDATED.
+  [key: string]: any;     // Allows for other, unspecified fields within a module entry.
+}
+
+interface BoardMetadata {
+  comments?: ModuleComment[];
+  visual?: { [key: string]: any };
+  tags?: string[];
+  [key: string]: any;
+}
+
+interface BoardImport {
+    url: string;
+    [key: string]: any;
+}
+
+interface BoardJSON {
path: packages/a2/scripts/sync-boards.ts
user: login: jameszhao00
body: Done
created_at: 2025-05-07T02:33:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,281 @@
+// Synchronizes TypeScript source files with corresponding `.bgl.json` board files.
+//
+// It's designed to:
+//   1. Only process `.bgl.json` files that ALREADY EXIST. It does not create new ones.
+//   2. For each existing board file, find its corresponding TypeScript source directory.
+//   3. For each TypeScript file found, if a module entry with the same key ALREADY EXISTS
+//      in the board's JSON, it will update ONLY two specific fields:
+//      - `modules[moduleKey].code` (with transpiled JavaScript)
+//      - `modules[moduleKey].metadata.source.code` (with raw TypeScript content)
+//   4. If a TypeScript file is found but its corresponding module key does not exist in the
+//      JSON, that TS file is SKIPPED, and no new module entry is created.
+//   5. All other data within the JSON (other module metadata, nodes, edges, board-level
+//      metadata, imports, exports, etc.) is PRESERVED.
+//   6. The script saves a board file back only if actual changes to the targeted fields
+//      were made.
+
+import * as fs from 'node:fs';
+import * as path from 'node:path';
+import * as ts from 'typescript';
+import { globSync } from 'glob';
+
+// --- Type Definitions for Board JSON Structure ---
+// These interfaces describe the expected (or at least parts of) structure of the .bgl.json files.
+// They help ensure the script interacts with the JSON data in a type-safe manner.
+
+interface ModuleSource {
+  code: string;       // Raw TypeScript content will be placed here.
+  language: string;   // Expected to be "typescript"; will be set if `source` object is created.
+  [key: string]: any; // Allows for other, unspecified fields within the source object.
+}
+
+interface ModuleMetadataVisual {
+  x?: number;
+  y?: number;
+  collapsed?: string;
+  outputHeight?: number;
+  [key: string]: any;
+}
+
+interface ModuleCommentMetadata {
+    title?: string;
+    visual?: ModuleMetadataVisual;
+    [key: string]: any;
+}
+
+interface ModuleComment {
+    id: string;
+    text: string;
+    metadata: ModuleCommentMetadata;
+}
+
+interface ModuleMetadata {
+  title?: string;         // This field will be PRESERVED if it exists.
+  description?: string;   // This field will be PRESERVED if it exists.
+  source?: ModuleSource;  // The `.code` sub-field here will be UPDATED.
+  runnable?: boolean;     // This field will be PRESERVED if it exists.
+  [key: string]: any;     // Allows for other, unspecified fields within module metadata.
+}
+
+interface ModuleEntry {
+  code: string;           // This field (transpiled JS) will be UPDATED.
+  metadata?: ModuleMetadata;// Its `.source.code` sub-field will be UPDATED.
+  [key: string]: any;     // Allows for other, unspecified fields within a module entry.
+}
+
+interface BoardMetadata {
+  comments?: ModuleComment[];
+  visual?: { [key: string]: any };
+  tags?: string[];
+  [key: string]: any;
+}
+
+interface BoardImport {
+    url: string;
+    [key: string]: any;
+}
+
+interface BoardJSON {
+  title: string;
+  description: string;
+  version: string;
+  nodes: any[];           // Preserved as-is.
+  edges: any[];           // Preserved as-is.
+  metadata: BoardMetadata;// Preserved as-is.
+  modules: {
+    [moduleKey: string]: ModuleEntry; // Entries here are candidates for targeted updates.
+  };
+  imports: {
+    [importKey: string]: BoardImport; // Preserved as-is.
+  };
+  exports: string[];      // Preserved as-is.
+  [key: string]: any;     // Allows for other top-level fields in the JSON.
+}
+
+// --- Main Processing Logic ---
+
+/**
+ * Processes a single .bgl.json board file.
+ * - Skips if the board file doesn't exist.
+ * - Reads the board JSON.
+ * - Finds corresponding TS files in `bgl/src/BOARD_NAME/`.
+ * - For each TS file matching an existing module key in the JSON:
+ * - Updates `module.code` and `module.metadata.source.code`.
+ * - Preserves all other data.
+ * - Writes back to the file ONLY if modifications were made.
+ * @param boardFilePath Absolute path to the .bgl.json file.
+ */
+async function processBoard(boardFilePath: string): Promise<void> {
+  const boardFileName = path.basename(boardFilePath);
+  const boardName = path.basename(boardFileName, '.bgl.json');
+
+  // Edge Case Handling: Script only operates on .bgl.json files that already exist.
+  // It will not create a new .bgl.json file if one isn't found.
+  if (!fs.existsSync(boardFilePath)) {
+    console.log(`[${boardName}] Skipping: Board file ${boardFileName} does not exist at ${boardFilePath}.`);
+    return;
+  }
+
+  console.log(`[${boardName}] Processing existing board file...`);
+
+  let boardJsonData: BoardJSON;
+  try {
+    // Read the existing JSON content.
+    boardJsonData = JSON.parse(fs.readFileSync(boardFilePath, 'utf-8')) as BoardJSON;
+  } catch (error: any) {
+    // Edge Case Handling: If JSON is malformed, log error and skip this board.
+    console.error(`[${boardName}] Error parsing existing JSON: ${error.message}. Please check its format. Skipping this board.`);
+    return;
+  }
+
+  // Ensure 'modules' object exists on the loaded JSON structure.
+  // This is a safeguard, though well-formed boards should have it.
+  boardJsonData.modules = boardJsonData.modules || {};
path: packages/a2/scripts/sync-boards.ts
user: login: jameszhao00
body: Fixed to skip syncing the modules section, if the modules section is empty.
created_at: 2025-05-07T02:33:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,281 @@
+// Synchronizes TypeScript source files with corresponding `.bgl.json` board files.
+//
+// It's designed to:
+//   1. Only process `.bgl.json` files that ALREADY EXIST. It does not create new ones.
+//   2. For each existing board file, find its corresponding TypeScript source directory.
+//   3. For each TypeScript file found, if a module entry with the same key ALREADY EXISTS
+//      in the board's JSON, it will update ONLY two specific fields:
+//      - `modules[moduleKey].code` (with transpiled JavaScript)
+//      - `modules[moduleKey].metadata.source.code` (with raw TypeScript content)
+//   4. If a TypeScript file is found but its corresponding module key does not exist in the
+//      JSON, that TS file is SKIPPED, and no new module entry is created.
+//   5. All other data within the JSON (other module metadata, nodes, edges, board-level
+//      metadata, imports, exports, etc.) is PRESERVED.
+//   6. The script saves a board file back only if actual changes to the targeted fields
+//      were made.
+
+import * as fs from 'node:fs';
+import * as path from 'node:path';
+import * as ts from 'typescript';
+import { globSync } from 'glob';
+
+// --- Type Definitions for Board JSON Structure ---
+// These interfaces describe the expected (or at least parts of) structure of the .bgl.json files.
+// They help ensure the script interacts with the JSON data in a type-safe manner.
+
+interface ModuleSource {
+  code: string;       // Raw TypeScript content will be placed here.
+  language: string;   // Expected to be "typescript"; will be set if `source` object is created.
+  [key: string]: any; // Allows for other, unspecified fields within the source object.
+}
+
+interface ModuleMetadataVisual {
+  x?: number;
+  y?: number;
+  collapsed?: string;
+  outputHeight?: number;
+  [key: string]: any;
+}
+
+interface ModuleCommentMetadata {
+    title?: string;
+    visual?: ModuleMetadataVisual;
+    [key: string]: any;
+}
+
+interface ModuleComment {
+    id: string;
+    text: string;
+    metadata: ModuleCommentMetadata;
+}
+
+interface ModuleMetadata {
+  title?: string;         // This field will be PRESERVED if it exists.
+  description?: string;   // This field will be PRESERVED if it exists.
+  source?: ModuleSource;  // The `.code` sub-field here will be UPDATED.
+  runnable?: boolean;     // This field will be PRESERVED if it exists.
+  [key: string]: any;     // Allows for other, unspecified fields within module metadata.
+}
+
+interface ModuleEntry {
+  code: string;           // This field (transpiled JS) will be UPDATED.
+  metadata?: ModuleMetadata;// Its `.source.code` sub-field will be UPDATED.
+  [key: string]: any;     // Allows for other, unspecified fields within a module entry.
+}
+
+interface BoardMetadata {
+  comments?: ModuleComment[];
+  visual?: { [key: string]: any };
+  tags?: string[];
+  [key: string]: any;
+}
+
+interface BoardImport {
+    url: string;
+    [key: string]: any;
+}
+
+interface BoardJSON {
+  title: string;
+  description: string;
+  version: string;
+  nodes: any[];           // Preserved as-is.
+  edges: any[];           // Preserved as-is.
+  metadata: BoardMetadata;// Preserved as-is.
+  modules: {
+    [moduleKey: string]: ModuleEntry; // Entries here are candidates for targeted updates.
+  };
+  imports: {
+    [importKey: string]: BoardImport; // Preserved as-is.
+  };
+  exports: string[];      // Preserved as-is.
+  [key: string]: any;     // Allows for other top-level fields in the JSON.
+}
+
+// --- Main Processing Logic ---
+
+/**
+ * Processes a single .bgl.json board file.
+ * - Skips if the board file doesn't exist.
+ * - Reads the board JSON.
+ * - Finds corresponding TS files in `bgl/src/BOARD_NAME/`.
+ * - For each TS file matching an existing module key in the JSON:
+ * - Updates `module.code` and `module.metadata.source.code`.
+ * - Preserves all other data.
+ * - Writes back to the file ONLY if modifications were made.
+ * @param boardFilePath Absolute path to the .bgl.json file.
+ */
+async function processBoard(boardFilePath: string): Promise<void> {
+  const boardFileName = path.basename(boardFilePath);
+  const boardName = path.basename(boardFileName, '.bgl.json');
+
+  // Edge Case Handling: Script only operates on .bgl.json files that already exist.
+  // It will not create a new .bgl.json file if one isn't found.
+  if (!fs.existsSync(boardFilePath)) {
+    console.log(`[${boardName}] Skipping: Board file ${boardFileName} does not exist at ${boardFilePath}.`);
+    return;
+  }
+
+  console.log(`[${boardName}] Processing existing board file...`);
+
+  let boardJsonData: BoardJSON;
+  try {
+    // Read the existing JSON content.
+    boardJsonData = JSON.parse(fs.readFileSync(boardFilePath, 'utf-8')) as BoardJSON;
+  } catch (error: any) {
+    // Edge Case Handling: If JSON is malformed, log error and skip this board.
+    console.error(`[${boardName}] Error parsing existing JSON: ${error.message}. Please check its format. Skipping this board.`);
+    return;
+  }
+
+  // Ensure 'modules' object exists on the loaded JSON structure.
+  // This is a safeguard, though well-formed boards should have it.
+  boardJsonData.modules = boardJsonData.modules || {};
+
+  // Define paths relative to the project root where the script is expected to run.
+  const PROJECT_ROOT = process.cwd();
+  const BGL_ROOT_DIR = path.join(PROJECT_ROOT, 'bgl');
+  const SRC_ROOT_DIR = path.join(BGL_ROOT_DIR, 'src');
+
+  // Convention: TypeScript source files for a board are located in a subdirectory
+  // named after the board within `bgl/src/`.
+  const tsSourceDir = path.join(SRC_ROOT_DIR, boardName);
+  let boardWasModified = false; // Flag to track if any actual changes are made to this board.
+
+  // Check if the corresponding TypeScript source directory exists.
+  if (fs.existsSync(tsSourceDir) && fs.lstatSync(tsSourceDir).isDirectory()) {
+    // Find all .ts files in the board's source directory.
+    const tsFiles = globSync('*.ts', { cwd: tsSourceDir, absolute: false });
+
+    if (tsFiles.length === 0) {
+      console.log(`  [${boardName}] No TypeScript files found in ${tsSourceDir}.`);
+    }
+
+    for (const tsFile of tsFiles) { // tsFile is relative path like 'main.ts'
+      const moduleKey = path.basename(tsFile, '.ts'); // e.g., 'main' from 'main.ts'
+      const fullTsFilePath = path.join(tsSourceDir, tsFile);
+
+      // --- Core Logic: Only update if moduleKey already exists in JSON ---
+      // If a .ts file exists but there's no corresponding entry in the .bgl.json's 'modules'
+      // object, this .ts file is SKIPPED. No new module entry will be created in the JSON.
+      // This ensures we only update targeted fields of PRE-EXISTING modules.
+      if (!boardJsonData.modules[moduleKey]) {
+        console.log(`  [${boardName}] Skipping TS file "${tsFile}": Module key "${moduleKey}" not found in existing JSON. No new module will be created.`);
+        continue;
+      }
+
+      console.log(`  [${boardName}] Updating existing module "${moduleKey}" from "${tsFile}"...`);
+      const moduleEntry = boardJsonData.modules[moduleKey]; // Get reference to existing module entry.
+      const tsContent = fs.readFileSync(fullTsFilePath, 'utf-8'); // Read raw TS content.
+
+      // Configure TypeScript transpilation.
+      // IMPORTANT: Align these options with your project's tsconfig.json for consistency,
+      // or enhance script to load compiler options from tsconfig.json.
+      const compilerOptions: ts.CompilerOptions = {
path: packages/a2/scripts/sync-boards.ts
user: login: jameszhao00
body: Changed to load the tsconfig from the path below, and transpile with that

```  
const tsConfigPath = ts.findConfigFile(
    process.cwd(),
    ts.sys.fileExists,
    "tsconfig.json"
  );
```
created_at: 2025-05-07T02:34:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,10 +57,13 @@
   "homepage": "https://github.com/breadboard-ai/breadboard#readme",
   "devDependencies": {
     "@google-cloud/firestore": "^7.11.0",
+    "@types/glob": "^8.1.0",
     "@types/node": "^22.0.0",
     "@typescript-eslint/eslint-plugin": "^7.18.0",
     "@typescript-eslint/parser": "^7.18.0",
     "dotenv": "^16.4.7",
+    "glob": "^11.0.2",
+    "ts-node": "^10.9.2",
path: packages/a2/package.json
user: login: dglazkov
body: remove ts-node here
created_at: 2025-05-07T02:46:59Z
author_association: COLLABORATOR
diff_hunk: @@ -56,10 +57,13 @@
   "homepage": "https://github.com/breadboard-ai/breadboard#readme",
   "devDependencies": {
     "@google-cloud/firestore": "^7.11.0",
+    "@types/glob": "^8.1.0",
     "@types/node": "^22.0.0",
     "@typescript-eslint/eslint-plugin": "^7.18.0",
     "@typescript-eslint/parser": "^7.18.0",
     "dotenv": "^16.4.7",
+    "glob": "^11.0.2",
+    "ts-node": "^10.9.2",
path: packages/a2/package.json
user: login: dglazkov
body: Also, please include package-lock.json -- otherwise, the CI won't let you land.
created_at: 2025-05-07T02:47:25Z
author_association: COLLABORATOR
diff_hunk: @@ -56,10 +57,13 @@
   "homepage": "https://github.com/breadboard-ai/breadboard#readme",
   "devDependencies": {
     "@google-cloud/firestore": "^7.11.0",
+    "@types/glob": "^8.1.0",
     "@types/node": "^22.0.0",
     "@typescript-eslint/eslint-plugin": "^7.18.0",
     "@typescript-eslint/parser": "^7.18.0",
     "dotenv": "^16.4.7",
+    "glob": "^11.0.2",
+    "ts-node": "^10.9.2",
path: packages/a2/package.json
user: login: jameszhao00
body: Done.
created_at: 2025-05-07T04:05:57Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-05-05T18:19:21Z
body: ###  ⚠️  No Changeset found

Latest commit: e7bf6dc961133c999e71f1de91d3670c826a7690

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/jameszhao00/breadboard/new/bgl-sync-script?filename=.changeset/cool-files-pull.md&value=---%0A%22%40breadboard-ai%2Fa2%22%3A%20patch%0A---%0A%0ABgl%20sync%20script%0A)


author_association: NONE
user: login: jameszhao00
created_at: 2025-05-07T02:35:18Z
body: > This is great, modulo a couple of nits. Thank you for doing the grungy bits ❤️

No worries! Made the requested changes and regened the bgl files. I've also updated the gemini summary of bgl.json changes.
author_association: CONTRIBUTOR