diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9bca386f..fb90bc1a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -150,7 +150,19 @@ jobs: deploy: name: Deploy → Vercel - needs: [library, website, cockpit, cockpit-examples-build, cockpit-smoke, cockpit-secret-integration, cockpit-deploy-smoke, chat-agent-smoke, cockpit-e2e, website-e2e] + needs: + [ + library, + website, + cockpit, + cockpit-examples-build, + cockpit-smoke, + cockpit-secret-integration, + cockpit-deploy-smoke, + chat-agent-smoke, + cockpit-e2e, + website-e2e, + ] runs-on: ubuntu-latest # Only deploy on pushes to main, not on pull requests if: github.ref == 'refs/heads/main' && github.event_name == 'push' @@ -297,8 +309,10 @@ jobs: cache: npm - run: npm ci - run: npx playwright install --with-deps chromium - - name: Verify LangGraph backends - run: npx tsx scripts/verify-langgraph-deployments.ts + - name: Verify shared LangGraph backend + run: npx tsx scripts/verify-shared-deployment.ts + env: + LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }} - name: Run production smoke tests run: npx playwright test apps/cockpit/e2e/production-smoke.spec.ts --reporter=list env: diff --git a/.github/workflows/deploy-langgraph.yml b/.github/workflows/deploy-langgraph.yml index eedcd60d7..33673b68f 100644 --- a/.github/workflows/deploy-langgraph.yml +++ b/.github/workflows/deploy-langgraph.yml @@ -4,89 +4,32 @@ on: push: branches: [main] paths: - - 'cockpit/**/python/**' + - 'cockpit/langgraph/**/python/**' + - 'cockpit/deep-agents/**/python/**' + - 'apps/cockpit/scripts/capability-registry.ts' + - 'scripts/generate-shared-deployment-config.ts' + - 'deployments/shared-dev/langgraph.json' workflow_dispatch: inputs: capability: - description: 'Capability path (e.g., langgraph/streaming)' + description: 'Deprecated and ignored by the shared deployment flow' required: false type: string jobs: deploy: - name: Deploy to LangGraph Cloud + name: Deploy shared cockpit-dev to LangGraph Cloud runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: - - name: streaming - path: cockpit/langgraph/streaming/python - - name: persistence - path: cockpit/langgraph/persistence/python - - name: interrupts - path: cockpit/langgraph/interrupts/python - - name: memory - path: cockpit/langgraph/memory/python - - name: durable-execution - path: cockpit/langgraph/durable-execution/python - - name: subgraphs - path: cockpit/langgraph/subgraphs/python - - name: time-travel - path: cockpit/langgraph/time-travel/python - - name: deployment-runtime - path: cockpit/langgraph/deployment-runtime/python - - name: planning - path: cockpit/deep-agents/planning/python - - name: filesystem - path: cockpit/deep-agents/filesystem/python - - name: da-subagents - path: cockpit/deep-agents/subagents/python - - name: da-memory - path: cockpit/deep-agents/memory/python - - name: skills - path: cockpit/deep-agents/skills/python - - name: sandboxes - path: cockpit/deep-agents/sandboxes/python - # Chat capabilities - - name: c-a2ui - path: cockpit/chat/a2ui/python - - name: c-debug - path: cockpit/chat/debug/python - - name: c-generative-ui - path: cockpit/chat/generative-ui/python - - name: c-input - path: cockpit/chat/input/python - - name: c-interrupts - path: cockpit/chat/interrupts/python - - name: c-messages - path: cockpit/chat/messages/python - - name: c-subagents - path: cockpit/chat/subagents/python - - name: c-theming - path: cockpit/chat/theming/python - - name: c-threads - path: cockpit/chat/threads/python - - name: c-timeline - path: cockpit/chat/timeline/python - - name: c-tool-calls - path: cockpit/chat/tool-calls/python - # Render capabilities - - name: r-computed-functions - path: cockpit/render/computed-functions/python - - name: r-element-rendering - path: cockpit/render/element-rendering/python - - name: r-registry - path: cockpit/render/registry/python - - name: r-repeat-loops - path: cockpit/render/repeat-loops/python - - name: r-spec-rendering - path: cockpit/render/spec-rendering/python - - name: r-state-management - path: cockpit/render/state-management/python steps: - uses: actions/checkout@v6.0.2 + - uses: actions/setup-node@v6.3.0 + with: + node-version: 22 + cache: npm + + - run: npm ci + - uses: actions/setup-python@v5 with: python-version: '3.12' @@ -97,19 +40,16 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Write .env for deployment - if: | - github.event_name == 'workflow_dispatch' && (inputs.capability == '' || contains(matrix.path, inputs.capability)) - || github.event_name == 'push' - working-directory: ${{ matrix.path }} + - name: Generate shared deployment manifest + run: npx tsx scripts/generate-shared-deployment-config.ts + + - name: Write .env for shared deployment run: | - echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" > .env + mkdir -p deployments/shared-dev + echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" > deployments/shared-dev/.env - - name: Deploy ${{ matrix.name }} - if: | - github.event_name == 'workflow_dispatch' && (inputs.capability == '' || contains(matrix.path, inputs.capability)) - || github.event_name == 'push' - working-directory: ${{ matrix.path }} - run: uv run --with langgraph-cli langgraph deploy --name ${{ matrix.name }} --no-wait + - name: Deploy cockpit-dev + working-directory: deployments/shared-dev + run: uv run --with langgraph-cli langgraph deploy --config langgraph.json --name cockpit-dev --no-wait env: LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }} diff --git a/AGENTS.md b/AGENTS.md index cfcceb6ee..41e596c8f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,6 +47,7 @@ This file is for agents working in this repository. It is contributor-facing, no - The workspace is Nx-based. Prefer project-scoped commands over broad workspace runs unless the task actually needs broader verification. - Inspect `project.json`, `nx.json`, and existing scripts before inventing commands. - If you need Nx-specific syntax or behavior and it is not obvious from local config, verify it from current Nx docs rather than relying on memory. +- The intended always-on LangSmith footprint is one shared cockpit dev deployment. Active capability keys in `deployment-urls.json` may all point at the same URL, and render demos stay local/static. - Respect generated and public-facing context files. If the task changes docs, API surface, positioning, or package guidance, check whether agent context or docs should be regenerated. ## Docs and Generated Context diff --git a/apps/cockpit/scripts/capability-registry.ts b/apps/cockpit/scripts/capability-registry.ts index a5691aaea..7c10e714b 100644 --- a/apps/cockpit/scripts/capability-registry.ts +++ b/apps/cockpit/scripts/capability-registry.ts @@ -23,7 +23,7 @@ export const capabilities: readonly Capability[] = [ { id: 'deployment-runtime', product: 'langgraph', topic: 'deployment-runtime', angularProject: 'cockpit-langgraph-deployment-runtime-angular', port: 4307, pythonDir: 'cockpit/langgraph/deployment-runtime/python', graphName: 'deployment-runtime' }, { id: 'planning', product: 'deep-agents', topic: 'planning', angularProject: 'cockpit-deep-agents-planning-angular', port: 4310, pythonDir: 'cockpit/deep-agents/planning/python', graphName: 'planning' }, { id: 'filesystem', product: 'deep-agents', topic: 'filesystem', angularProject: 'cockpit-deep-agents-filesystem-angular', port: 4311, pythonDir: 'cockpit/deep-agents/filesystem/python', graphName: 'filesystem' }, - { id: 'da-subagents', product: 'deep-agents', topic: 'subagents', angularProject: 'cockpit-deep-agents-subagents-angular', port: 4312, pythonDir: 'cockpit/deep-agents/subagents/python', graphName: 'da-subagents' }, + { id: 'da-subagents', product: 'deep-agents', topic: 'subagents', angularProject: 'cockpit-deep-agents-subagents-angular', port: 4312, pythonDir: 'cockpit/deep-agents/subagents/python', graphName: 'subagents' }, { id: 'da-memory', product: 'deep-agents', topic: 'memory', angularProject: 'cockpit-deep-agents-memory-angular', port: 4313, pythonDir: 'cockpit/deep-agents/memory/python', graphName: 'da-memory' }, { id: 'skills', product: 'deep-agents', topic: 'skills', angularProject: 'cockpit-deep-agents-skills-angular', port: 4314, pythonDir: 'cockpit/deep-agents/skills/python', graphName: 'skills' }, { id: 'sandboxes', product: 'deep-agents', topic: 'sandboxes', angularProject: 'cockpit-deep-agents-sandboxes-angular', port: 4315, pythonDir: 'cockpit/deep-agents/sandboxes/python', graphName: 'sandboxes' }, @@ -35,17 +35,17 @@ export const capabilities: readonly Capability[] = [ { id: 'repeat-loops', product: 'render', topic: 'repeat-loops', angularProject: 'cockpit-render-repeat-loops-angular', port: 4405, pythonDir: 'cockpit/render/repeat-loops/python', graphName: 'repeat-loops' }, { id: 'computed-functions', product: 'render', topic: 'computed-functions', angularProject: 'cockpit-render-computed-functions-angular', port: 4406, pythonDir: 'cockpit/render/computed-functions/python', graphName: 'computed-functions' }, // Chat capabilities - { id: 'c-messages', product: 'chat', topic: 'messages', angularProject: 'cockpit-chat-messages-angular', port: 4501, pythonDir: 'cockpit/chat/messages/python', graphName: 'c-messages' }, - { id: 'c-input', product: 'chat', topic: 'input', angularProject: 'cockpit-chat-input-angular', port: 4502, pythonDir: 'cockpit/chat/input/python', graphName: 'c-input' }, - { id: 'c-interrupts', product: 'chat', topic: 'interrupts', angularProject: 'cockpit-chat-interrupts-angular', port: 4503, pythonDir: 'cockpit/chat/interrupts/python', graphName: 'c-interrupts' }, - { id: 'c-tool-calls', product: 'chat', topic: 'tool-calls', angularProject: 'cockpit-chat-tool-calls-angular', port: 4504, pythonDir: 'cockpit/chat/tool-calls/python', graphName: 'c-tool-calls' }, - { id: 'c-subagents', product: 'chat', topic: 'subagents', angularProject: 'cockpit-chat-subagents-angular', port: 4505, pythonDir: 'cockpit/chat/subagents/python', graphName: 'c-subagents' }, - { id: 'c-threads', product: 'chat', topic: 'threads', angularProject: 'cockpit-chat-threads-angular', port: 4506, pythonDir: 'cockpit/chat/threads/python', graphName: 'c-threads' }, - { id: 'c-timeline', product: 'chat', topic: 'timeline', angularProject: 'cockpit-chat-timeline-angular', port: 4507, pythonDir: 'cockpit/chat/timeline/python', graphName: 'c-timeline' }, - { id: 'c-generative-ui', product: 'chat', topic: 'generative-ui', angularProject: 'cockpit-chat-generative-ui-angular', port: 4508, pythonDir: 'cockpit/chat/generative-ui/python', graphName: 'c-generative-ui' }, - { id: 'c-debug', product: 'chat', topic: 'debug', angularProject: 'cockpit-chat-debug-angular', port: 4509, pythonDir: 'cockpit/chat/debug/python', graphName: 'c-debug' }, - { id: 'c-theming', product: 'chat', topic: 'theming', angularProject: 'cockpit-chat-theming-angular', port: 4510, pythonDir: 'cockpit/chat/theming/python', graphName: 'c-theming' }, - { id: 'c-a2ui', product: 'chat', topic: 'a2ui', angularProject: 'cockpit-chat-a2ui-angular', port: 4511, pythonDir: 'cockpit/chat/a2ui/python', graphName: 'c-a2ui' }, + { id: 'c-messages', product: 'chat', topic: 'messages', angularProject: 'cockpit-chat-messages-angular', port: 4501, pythonDir: 'cockpit/langgraph/streaming/python', graphName: 'c-messages' }, + { id: 'c-input', product: 'chat', topic: 'input', angularProject: 'cockpit-chat-input-angular', port: 4502, pythonDir: 'cockpit/langgraph/streaming/python', graphName: 'c-input' }, + { id: 'c-interrupts', product: 'chat', topic: 'interrupts', angularProject: 'cockpit-chat-interrupts-angular', port: 4503, pythonDir: 'cockpit/langgraph/streaming/python', graphName: 'c-interrupts' }, + { id: 'c-tool-calls', product: 'chat', topic: 'tool-calls', angularProject: 'cockpit-chat-tool-calls-angular', port: 4504, pythonDir: 'cockpit/langgraph/streaming/python', graphName: 'c-tool-calls' }, + { id: 'c-subagents', product: 'chat', topic: 'subagents', angularProject: 'cockpit-chat-subagents-angular', port: 4505, pythonDir: 'cockpit/langgraph/streaming/python', graphName: 'c-subagents' }, + { id: 'c-threads', product: 'chat', topic: 'threads', angularProject: 'cockpit-chat-threads-angular', port: 4506, pythonDir: 'cockpit/langgraph/streaming/python', graphName: 'c-threads' }, + { id: 'c-timeline', product: 'chat', topic: 'timeline', angularProject: 'cockpit-chat-timeline-angular', port: 4507, pythonDir: 'cockpit/langgraph/streaming/python', graphName: 'c-timeline' }, + { id: 'c-generative-ui', product: 'chat', topic: 'generative-ui', angularProject: 'cockpit-chat-generative-ui-angular', port: 4508, pythonDir: 'cockpit/langgraph/streaming/python', graphName: 'c-generative-ui' }, + { id: 'c-debug', product: 'chat', topic: 'debug', angularProject: 'cockpit-chat-debug-angular', port: 4509, pythonDir: 'cockpit/langgraph/streaming/python', graphName: 'c-debug' }, + { id: 'c-theming', product: 'chat', topic: 'theming', angularProject: 'cockpit-chat-theming-angular', port: 4510, pythonDir: 'cockpit/langgraph/streaming/python', graphName: 'c-theming' }, + { id: 'c-a2ui', product: 'chat', topic: 'a2ui', angularProject: 'cockpit-chat-a2ui-angular', port: 4511, pythonDir: 'cockpit/langgraph/streaming/python', graphName: 'c-a2ui' }, ] as const; export function findCapability(id: string): Capability | undefined { diff --git a/apps/cockpit/scripts/generate-combined-langgraph.ts b/apps/cockpit/scripts/generate-combined-langgraph.ts index 1c1f44abf..b6cf57e98 100644 --- a/apps/cockpit/scripts/generate-combined-langgraph.ts +++ b/apps/cockpit/scripts/generate-combined-langgraph.ts @@ -1,13 +1,49 @@ -import { writeFileSync } from 'fs'; +import { readFileSync, writeFileSync } from 'fs'; import { resolve } from 'path'; import { capabilities } from './capability-registry'; +type LangGraphManifest = { + graphs: Record; +}; + +const manifestCache = new Map(); +function readManifest(pythonDir: string): LangGraphManifest { + const existing = manifestCache.get(pythonDir); + if (existing) { + return existing; + } + + const manifestPath = resolve(process.cwd(), pythonDir, 'langgraph.json'); + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as LangGraphManifest; + manifestCache.set(pythonDir, manifest); + return manifest; +} + +function normalizeEntrypoint(pythonDir: string, graphName: string): string { + const manifest = readManifest(pythonDir); + const entrypoint = manifest.graphs[graphName] + ?? (() => { + const manifestEntries = Object.entries(manifest.graphs); + if (manifestEntries.length === 1) { + return manifestEntries[0]?.[1]; + } + return undefined; + })(); + if (!entrypoint) { + throw new Error(`Missing graph '${graphName}' in ${pythonDir}/langgraph.json`); + } + + return entrypoint.startsWith('./') ? entrypoint.slice(2) : entrypoint; +} + const graphs: Record = {}; +const dependencies = new Set(); for (const c of capabilities) { - graphs[c.graphName] = `./${c.pythonDir}/src/graph.py:graph`; + graphs[c.graphName] = `./${c.pythonDir}/${normalizeEntrypoint(c.pythonDir, c.graphName)}`; + dependencies.add(`./${c.pythonDir}`); } -const config = { graphs, dependencies: capabilities.map((c) => `./${c.pythonDir}/pyproject.toml`), env: '.env' }; +const config = { graphs, dependencies: [...dependencies], env: '.env' }; const out = resolve(process.cwd(), 'langgraph-combined.json'); writeFileSync(out, JSON.stringify(config, null, 2) + '\n'); console.log(`Generated ${out} with ${Object.keys(graphs).length} graphs`); diff --git a/cockpit/chat/a2ui/angular/src/environments/environment.ts b/cockpit/chat/a2ui/angular/src/environments/environment.ts index f23ee1ad2..2919bf6d0 100644 --- a/cockpit/chat/a2ui/angular/src/environments/environment.ts +++ b/cockpit/chat/a2ui/angular/src/environments/environment.ts @@ -1,5 +1,5 @@ export const environment = { production: true, langGraphApiUrl: '/api', - a2uiAssistantId: 'a2ui_form', + a2uiAssistantId: 'c-a2ui', }; diff --git a/cockpit/chat/generative-ui/angular/src/environments/environment.ts b/cockpit/chat/generative-ui/angular/src/environments/environment.ts index 1cdf59a2a..e1cae1570 100644 --- a/cockpit/chat/generative-ui/angular/src/environments/environment.ts +++ b/cockpit/chat/generative-ui/angular/src/environments/environment.ts @@ -1,5 +1,5 @@ export const environment = { production: true, langGraphApiUrl: '/api', - generativeUiAssistantId: 'generative_ui', + generativeUiAssistantId: 'c-generative-ui', }; diff --git a/cockpit/langgraph/streaming/python/langgraph.json b/cockpit/langgraph/streaming/python/langgraph.json index a76b91047..c22d85663 100644 --- a/cockpit/langgraph/streaming/python/langgraph.json +++ b/cockpit/langgraph/streaming/python/langgraph.json @@ -1,7 +1,7 @@ { "graphs": { "streaming": "./src/graph.py:graph", - "generative_ui": "./src/chat_graphs.py:generative_ui", + "c-generative-ui": "./src/chat_graphs.py:generative_ui", "c-messages": "./src/chat_graphs.py:c_messages", "c-input": "./src/chat_graphs.py:c_input", "c-debug": "./src/chat_graphs.py:c_debug", @@ -11,7 +11,7 @@ "c-timeline": "./src/chat_graphs.py:c_timeline", "c-tool-calls": "./src/chat_graphs.py:c_tool_calls", "c-subagents": "./src/chat_graphs.py:c_subagents", - "a2ui_form": "./src/a2ui_graph.py:graph" + "c-a2ui": "./src/a2ui_graph.py:graph" }, "dependencies": [ "." diff --git a/deployment-urls.json b/deployment-urls.json index bd473eac0..0d70fb0f5 100644 --- a/deployment-urls.json +++ b/deployment-urls.json @@ -1,33 +1,16 @@ { - "streaming": "https://streaming-b01895ee8c8d5211967fba7a64c55db8.us.langgraph.app", - "persistence": "https://persistence-b4038c008b5e537787dda6a6774c8f91.us.langgraph.app", - "interrupts": "https://interrupts-8e1524d6d8fb558381eed4618129bc50.us.langgraph.app", - "memory": "https://memory-1b3234dbe2e55ba59010b3469be45a0a.us.langgraph.app", - "durable-execution": "https://durable-execution-123221d8b543545399d252dc6bd7de1b.us.langgraph.app", - "subgraphs": "https://subgraphs-c923bcb068c458b09d789f147875f426.us.langgraph.app", - "time-travel": "https://time-travel-f206148d75f45e75bf30002e68e1b14d.us.langgraph.app", - "deployment-runtime": "https://deployment-runtime-ce6aad33cc10505faca2b6137e76ba35.us.langgraph.app", - "planning": "https://planning-7ca04c65ce7650048ec0d16fb96a7638.us.langgraph.app", - "filesystem": "https://filesystem-2330285f57625bff8654bc026f70a6ae.us.langgraph.app", - "subagents": "https://da-subagents-31e4639441165df7848aaad426e61728.us.langgraph.app", - "da-memory": "https://da-memory-15f767adfa6f5cd48bd45a0fa4db29b5.us.langgraph.app", - "skills": "https://skills-802ff50f64325f1ea973cff1c97a49f9.us.langgraph.app", - "sandboxes": "https://sandboxes-8c70b6ac20265827aa92397299fcb9f7.us.langgraph.app", - "c-a2ui": "https://c-a2ui-8e9f0ef287d25d2fb134d7f881570d1c.us.langgraph.app", - "c-debug": "https://c-debug-98cf2d0aa084584c93cbd6c40ab260bd.us.langgraph.app", - "c-generative-ui": "https://c-generative-ui-8ad5bc56908f5a45b033bd8258d61bb1.us.langgraph.app", - "c-input": "https://c-input-5b25fd46dcdf5e0086fa4ef01349e332.us.langgraph.app", - "c-interrupts": "https://c-interrupts-fdb30735b73352ddbf58f7388ae5fce5.us.langgraph.app", - "c-messages": "https://c-messages-46ae9adfa13e5ebeae5b3b6e6a03038e.us.langgraph.app", - "c-subagents": "https://c-subagents-687f67b7a44257f096f9c700a9982b33.us.langgraph.app", - "c-theming": "https://c-theming-d76a27db156351b4936f4b6a995519db.us.langgraph.app", - "c-threads": "https://c-threads-d14fe73520c353c89c3c45529c284646.us.langgraph.app", - "c-timeline": "https://c-timeline-f53431e515ca54dea03f08f7f75e344c.us.langgraph.app", - "c-tool-calls": "https://c-tool-calls-6541936001185fb88f83888136d36b5b.us.langgraph.app", - "r-computed-functions": "https://r-computed-functions-d13875d7991f586f986f8fa78b3b5150.us.langgraph.app", - "r-element-rendering": "https://r-element-rendering-72a833e9aeb1504aa1b2f62283fe7d0d.us.langgraph.app", - "r-registry": "https://r-registry-8dfd9a501a9d554aad4a63b4b5eb7d63.us.langgraph.app", - "r-repeat-loops": "https://r-repeat-loops-3d57704b1c4b5af999beab9f300dc81e.us.langgraph.app", - "r-spec-rendering": "https://r-spec-rendering-92ee35b6e8565f0c87eed4e8de085f28.us.langgraph.app", - "r-state-management": "https://r-state-management-298d402c1f3b5a729ef1872e3d6b83be.us.langgraph.app" + "streaming": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app", + "persistence": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app", + "interrupts": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app", + "memory": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app", + "durable-execution": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app", + "subgraphs": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app", + "time-travel": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app", + "deployment-runtime": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app", + "planning": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app", + "filesystem": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app", + "subagents": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app", + "da-memory": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app", + "skills": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app", + "sandboxes": "https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app" } diff --git a/deployments/shared-dev/langgraph.json b/deployments/shared-dev/langgraph.json new file mode 100644 index 000000000..a337ed132 --- /dev/null +++ b/deployments/shared-dev/langgraph.json @@ -0,0 +1,47 @@ +{ + "graphs": { + "streaming": "./deps/streaming/src/graph.py:graph", + "c-generative-ui": "./deps/streaming/src/chat_graphs.py:generative_ui", + "c-messages": "./deps/streaming/src/chat_graphs.py:c_messages", + "c-input": "./deps/streaming/src/chat_graphs.py:c_input", + "c-debug": "./deps/streaming/src/chat_graphs.py:c_debug", + "c-interrupts": "./deps/streaming/src/chat_graphs.py:c_interrupts", + "c-theming": "./deps/streaming/src/chat_graphs.py:c_theming", + "c-threads": "./deps/streaming/src/chat_graphs.py:c_threads", + "c-timeline": "./deps/streaming/src/chat_graphs.py:c_timeline", + "c-tool-calls": "./deps/streaming/src/chat_graphs.py:c_tool_calls", + "c-subagents": "./deps/streaming/src/chat_graphs.py:c_subagents", + "c-a2ui": "./deps/streaming/src/a2ui_graph.py:graph", + "persistence": "./deps/persistence/src/graph.py:graph", + "interrupts": "./deps/interrupts/src/graph.py:graph", + "memory": "./deps/memory/src/graph.py:graph", + "durable-execution": "./deps/durable-execution/src/graph.py:graph", + "subgraphs": "./deps/subgraphs/src/graph.py:graph", + "time-travel": "./deps/time-travel/src/graph.py:graph", + "deployment-runtime": "./deps/deployment-runtime/src/graph.py:graph", + "planning": "./deps/planning/src/graph.py:graph", + "filesystem": "./deps/filesystem/src/graph.py:graph", + "subagents": "./deps/da-subagents/src/graph.py:graph", + "da-memory": "./deps/da-memory/src/graph.py:graph", + "skills": "./deps/skills/src/graph.py:graph", + "sandboxes": "./deps/sandboxes/src/graph.py:graph" + }, + "dependencies": [ + "./deps/da-memory", + "./deps/da-subagents", + "./deps/deployment-runtime", + "./deps/durable-execution", + "./deps/filesystem", + "./deps/interrupts", + "./deps/memory", + "./deps/persistence", + "./deps/planning", + "./deps/sandboxes", + "./deps/skills", + "./deps/streaming", + "./deps/subgraphs", + "./deps/time-travel" + ], + "python_version": "3.12", + "env": ".env" +} diff --git a/scripts/examples-middleware.ts b/scripts/examples-middleware.ts index 7a70ac4ae..f7e92d7ad 100644 --- a/scripts/examples-middleware.ts +++ b/scripts/examples-middleware.ts @@ -1,99 +1,82 @@ /** * Vercel Serverless Function proxy for LangGraph Cloud. * - * Deployed as api/[...path].js — catches all /api/* requests. - * Injects x-api-key header from LANGSMITH_API_KEY env var. - * Routes to the correct backend based on the Referer header. - */ -// Types only — Vercel provides these at runtime -type VercelRequest = { method?: string; headers: Record; body: unknown; url?: string; query: Record }; -type VercelResponse = { setHeader(k: string, v: string): void; status(code: number): VercelResponse; json(body: unknown): void; write(chunk: string): void; end(): void; send(body: string): void }; - -const DEPLOYMENT_URLS: Record = { - 'streaming': 'https://streaming-b01895ee8c8d5211967fba7a64c55db8.us.langgraph.app', - 'persistence': 'https://persistence-b4038c008b5e537787dda6a6774c8f91.us.langgraph.app', - 'interrupts': 'https://interrupts-8e1524d6d8fb558381eed4618129bc50.us.langgraph.app', - 'memory': 'https://memory-1b3234dbe2e55ba59010b3469be45a0a.us.langgraph.app', - 'durable-execution': 'https://durable-execution-123221d8b543545399d252dc6bd7de1b.us.langgraph.app', - 'subgraphs': 'https://subgraphs-c923bcb068c458b09d789f147875f426.us.langgraph.app', - 'time-travel': 'https://time-travel-f206148d75f45e75bf30002e68e1b14d.us.langgraph.app', - 'deployment-runtime': 'https://deployment-runtime-ce6aad33cc10505faca2b6137e76ba35.us.langgraph.app', - 'planning': 'https://planning-7ca04c65ce7650048ec0d16fb96a7638.us.langgraph.app', - 'filesystem': 'https://filesystem-2330285f57625bff8654bc026f70a6ae.us.langgraph.app', - 'da-subagents': 'https://da-subagents-31e4639441165df7848aaad426e61728.us.langgraph.app', - 'da-memory': 'https://da-memory-15f767adfa6f5cd48bd45a0fa4db29b5.us.langgraph.app', - 'skills': 'https://skills-802ff50f64325f1ea973cff1c97a49f9.us.langgraph.app', - 'sandboxes': 'https://sandboxes-8c70b6ac20265827aa92397299fcb9f7.us.langgraph.app', - // Chat capabilities - 'c-a2ui': 'https://c-a2ui-8e9f0ef287d25d2fb134d7f881570d1c.us.langgraph.app', - 'c-debug': 'https://c-debug-98cf2d0aa084584c93cbd6c40ab260bd.us.langgraph.app', - 'c-generative-ui': 'https://c-generative-ui-8ad5bc56908f5a45b033bd8258d61bb1.us.langgraph.app', - 'c-input': 'https://c-input-5b25fd46dcdf5e0086fa4ef01349e332.us.langgraph.app', - 'c-interrupts': 'https://c-interrupts-fdb30735b73352ddbf58f7388ae5fce5.us.langgraph.app', - 'c-messages': 'https://c-messages-46ae9adfa13e5ebeae5b3b6e6a03038e.us.langgraph.app', - 'c-subagents': 'https://c-subagents-687f67b7a44257f096f9c700a9982b33.us.langgraph.app', - 'c-theming': 'https://c-theming-d76a27db156351b4936f4b6a995519db.us.langgraph.app', - 'c-threads': 'https://c-threads-d14fe73520c353c89c3c45529c284646.us.langgraph.app', - 'c-timeline': 'https://c-timeline-f53431e515ca54dea03f08f7f75e344c.us.langgraph.app', - 'c-tool-calls': 'https://c-tool-calls-6541936001185fb88f83888136d36b5b.us.langgraph.app', - // Render capabilities - 'r-computed-functions': 'https://r-computed-functions-d13875d7991f586f986f8fa78b3b5150.us.langgraph.app', - 'r-element-rendering': 'https://r-element-rendering-72a833e9aeb1504aa1b2f62283fe7d0d.us.langgraph.app', - 'r-registry': 'https://r-registry-8dfd9a501a9d554aad4a63b4b5eb7d63.us.langgraph.app', - 'r-repeat-loops': 'https://r-repeat-loops-3d57704b1c4b5af999beab9f300dc81e.us.langgraph.app', - 'r-spec-rendering': 'https://r-spec-rendering-92ee35b6e8565f0c87eed4e8de085f28.us.langgraph.app', - 'r-state-management': 'https://r-state-management-298d402c1f3b5a729ef1872e3d6b83be.us.langgraph.app', + * Deployed as api/[...path].js - catches all /api/* requests. + * Injects x-api-key header from LANGSMITH_API_KEY env var. + * Routes active product paths to the shared cockpit dev backend based on the + * Referer header. + */ +// Types only - Vercel provides these at runtime +type VercelRequest = { + method?: string; + headers: Record; + body: unknown; + url?: string; + query: Record; }; - -const PATH_TO_KEY: Record = { - 'langgraph/streaming': 'streaming', - 'langgraph/persistence': 'persistence', - 'langgraph/interrupts': 'interrupts', - 'langgraph/memory': 'memory', - 'langgraph/durable-execution': 'durable-execution', - 'langgraph/subgraphs': 'subgraphs', - 'langgraph/time-travel': 'time-travel', - 'langgraph/deployment-runtime': 'deployment-runtime', - 'deep-agents/planning': 'planning', - 'deep-agents/filesystem': 'filesystem', - 'deep-agents/subagents': 'da-subagents', - 'deep-agents/memory': 'da-memory', - 'deep-agents/skills': 'skills', - 'deep-agents/sandboxes': 'sandboxes', - // Chat capabilities — routed through the streaming deployment which has - // all chat graphs consolidated in its langgraph.json (PR #113). - 'chat/a2ui': 'streaming', - 'chat/debug': 'streaming', - 'chat/generative-ui': 'streaming', - 'chat/input': 'streaming', - 'chat/interrupts': 'streaming', - 'chat/messages': 'streaming', - 'chat/subagents': 'streaming', - 'chat/theming': 'streaming', - 'chat/threads': 'streaming', - 'chat/timeline': 'streaming', - 'chat/tool-calls': 'streaming', - // Render capabilities - 'render/computed-functions': 'r-computed-functions', - 'render/element-rendering': 'r-element-rendering', - 'render/registry': 'r-registry', - 'render/repeat-loops': 'r-repeat-loops', - 'render/spec-rendering': 'r-spec-rendering', - 'render/state-management': 'r-state-management', +type VercelResponse = { + setHeader(k: string, v: string): void; + status(code: number): VercelResponse; + json(body: unknown): void; + write(chunk: string): void; + end(): void; + send(body: string): void; }; +const SHARED_DEPLOYMENT_URL = 'https://cockpit-dev-219a15942c545a00a03a9a41905d7fc2.us.langgraph.app'; + +const ACTIVE_PRODUCT_PATHS = new Set([ + 'langgraph/streaming', + 'langgraph/persistence', + 'langgraph/interrupts', + 'langgraph/memory', + 'langgraph/durable-execution', + 'langgraph/subgraphs', + 'langgraph/time-travel', + 'langgraph/deployment-runtime', + 'deep-agents/planning', + 'deep-agents/filesystem', + 'deep-agents/subagents', + 'deep-agents/memory', + 'deep-agents/skills', + 'deep-agents/sandboxes', + 'chat/messages', + 'chat/input', + 'chat/interrupts', + 'chat/tool-calls', + 'chat/subagents', + 'chat/threads', + 'chat/timeline', + 'chat/generative-ui', + 'chat/debug', + 'chat/theming', + 'chat/a2ui', +]); + +function isActiveProductPath(pathname: string): boolean { + const segments = pathname.split('/').filter(Boolean); + if (segments.length < 2) { + return false; + } + + return ACTIVE_PRODUCT_PATHS.has(`${segments[0]}/${segments[1]}`); +} + function resolveBackend(referer: string | undefined): string { - if (referer) { - try { - const url = new URL(referer); - const segments = url.pathname.split('/').filter(Boolean); - if (segments.length >= 2) { - const key = PATH_TO_KEY[`${segments[0]}/${segments[1]}`]; - if (key && DEPLOYMENT_URLS[key]) return DEPLOYMENT_URLS[key]; - } - } catch { /* invalid referer */ } + if (!referer) { + return SHARED_DEPLOYMENT_URL; } - return DEPLOYMENT_URLS['streaming']; + + try { + const url = new URL(referer); + if (isActiveProductPath(url.pathname)) { + return SHARED_DEPLOYMENT_URL; + } + } catch { + // Ignore invalid referers and fall back to the shared deployment. + } + + return SHARED_DEPLOYMENT_URL; } module.exports = async function handler(req: VercelRequest, res: VercelResponse) { @@ -115,16 +98,16 @@ module.exports = async function handler(req: VercelRequest, res: VercelResponse) const backendUrl = resolveBackend(req.headers.referer); - // Build target URL — extract path from req.url, stripping /api prefix + // Build target URL - extract path from req.url, stripping /api prefix. const parsedUrl = new URL(req.url ?? '', `https://${req.headers.host ?? 'localhost'}`); const apiPath = parsedUrl.pathname.replace(/^\/api/, '') || '/'; - // Strip the Vercel catch-all query param, keep any real query params + // Strip the Vercel catch-all query param, keep any real query params. parsedUrl.searchParams.delete('[...path]'); parsedUrl.searchParams.delete('[[...path]]'); const cleanSearch = parsedUrl.searchParams.toString() ? `?${parsedUrl.searchParams.toString()}` : ''; const targetUrl = `${backendUrl}${apiPath}${cleanSearch}`; - // Debug endpoint + // Debug endpoint. if (apiPath === '/_proxy_debug') { return res.status(200).json({ method: req.method, @@ -132,6 +115,7 @@ module.exports = async function handler(req: VercelRequest, res: VercelResponse) apiPath, targetUrl, backendUrl, + sharedDeployment: SHARED_DEPLOYMENT_URL, referer: req.headers.referer, query: req.query, hasApiKey: !!apiKey, @@ -141,7 +125,7 @@ module.exports = async function handler(req: VercelRequest, res: VercelResponse) console.log(`[proxy] ${req.method} ${req.url} → ${targetUrl}`); - // Forward headers, inject API key + // Forward headers, inject API key. const headers: Record = { 'x-api-key': apiKey, 'content-type': req.headers['content-type'] ?? 'application/json', @@ -154,13 +138,13 @@ module.exports = async function handler(req: VercelRequest, res: VercelResponse) body: req.method !== 'GET' && req.method !== 'HEAD' ? JSON.stringify(req.body) : undefined, }); - // Stream the response back + // Stream the response back. const contentType = response.headers.get('content-type') ?? 'application/json'; res.setHeader('content-type', contentType); res.status(response.status); if (contentType.includes('text/event-stream')) { - // SSE streaming — pipe the response body + // SSE streaming - pipe the response body. const reader = response.body?.getReader(); if (reader) { const decoder = new TextDecoder(); @@ -178,4 +162,4 @@ module.exports = async function handler(req: VercelRequest, res: VercelResponse) } catch (err) { res.status(502).json({ error: 'Proxy error', message: (err as Error).message }); } -} +}; diff --git a/scripts/generate-shared-deployment-config.ts b/scripts/generate-shared-deployment-config.ts new file mode 100644 index 000000000..0527b7bca --- /dev/null +++ b/scripts/generate-shared-deployment-config.ts @@ -0,0 +1,85 @@ +import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'; +import { dirname, resolve } from 'path'; +import { capabilities } from '../apps/cockpit/scripts/capability-registry'; + +type LangGraphManifest = { + graphs: Record; + dependencies?: string[]; + env?: string; + python_version?: string; +}; + +const rootDir = process.cwd(); +const deploymentDir = resolve(rootDir, 'deployments/shared-dev'); +const outputPath = resolve(deploymentDir, 'langgraph.json'); +const stagedDependenciesDir = resolve(deploymentDir, 'deps'); + +const readManifest = (filePath: string): LangGraphManifest => JSON.parse(readFileSync(filePath, 'utf8')) as LangGraphManifest; +const toDeploymentPath = (dependencyRoot: string, entrypoint: string): string => { + const normalizedEntrypoint = entrypoint.startsWith('./') ? entrypoint.slice(2) : entrypoint; + return `${dependencyRoot}/${normalizedEntrypoint}`; +}; + +const graphs: Record = {}; +const dependencies = new Set(); +const stagedDependencyRoots = new Map(); +const addGraph = (name: string, path: string): void => { + const existing = graphs[name]; + if (existing && existing !== path) { + throw new Error(`Conflicting graph path for ${name}: ${existing} !== ${path}`); + } + graphs[name] = path; +}; + +const stageDependency = (sourceRoot: string, alias: string): string => { + const existing = stagedDependencyRoots.get(sourceRoot); + if (existing) { + return existing; + } + + const sourceDir = resolve(rootDir, sourceRoot); + const stagedDir = resolve(stagedDependenciesDir, alias); + cpSync(sourceDir, stagedDir, { recursive: true }); + + const relativePath = `./deps/${alias}`; + stagedDependencyRoots.set(sourceRoot, relativePath); + dependencies.add(relativePath); + return relativePath; +}; + +rmSync(stagedDependenciesDir, { recursive: true, force: true }); +mkdirSync(stagedDependenciesDir, { recursive: true }); + +for (const capability of capabilities) { + if (capability.product !== 'langgraph' && capability.product !== 'deep-agents') { + continue; + } + + const manifestPath = resolve(rootDir, capability.pythonDir, 'langgraph.json'); + const manifest = readManifest(manifestPath); + if (!manifest.graphs) { + throw new Error(`Missing graphs in ${manifestPath}`); + } + const stagedDependencyRoot = stageDependency(capability.pythonDir, capability.id); + + for (const [graphName, entrypoint] of Object.entries(manifest.graphs)) { + addGraph(graphName, toDeploymentPath(stagedDependencyRoot, entrypoint)); + } +} + +const streamingManifestPath = resolve(rootDir, 'cockpit/langgraph/streaming/python/langgraph.json'); +const streamingManifest = readManifest(streamingManifestPath); +if (!streamingManifest.graphs) { + throw new Error(`Missing graphs in ${streamingManifestPath}`); +} + +const manifest: LangGraphManifest = { + graphs, + dependencies: [...dependencies].sort(), + python_version: streamingManifest.python_version ?? '3.12', + env: streamingManifest.env ?? '.env', +}; + +mkdirSync(dirname(outputPath), { recursive: true }); +writeFileSync(outputPath, `${JSON.stringify(manifest, null, 2)}\n`); +console.log(`Generated ${outputPath} with ${Object.keys(graphs).length} graphs`); diff --git a/scripts/update-angular-environments.ts b/scripts/update-angular-environments.ts index ca701931d..631926805 100644 --- a/scripts/update-angular-environments.ts +++ b/scripts/update-angular-environments.ts @@ -2,6 +2,9 @@ /** * Update all Angular production environment.ts files with LangGraph Cloud URLs * from deployment-urls.json. + * + * The registry may map every active capability to the same shared deployment + * URL while the final shared LangSmith deployment is being finalized. */ import { readFileSync, writeFileSync } from 'fs'; import { resolve } from 'path'; @@ -26,12 +29,31 @@ const capabilities = [ { dir: 'cockpit/deep-agents/memory/angular', assistantId: 'da-memory', field: 'streamingAssistantId' }, { dir: 'cockpit/deep-agents/skills/angular', assistantId: 'skills', field: 'streamingAssistantId' }, { dir: 'cockpit/deep-agents/sandboxes/angular', assistantId: 'sandboxes', field: 'streamingAssistantId' }, + { dir: 'cockpit/chat/messages/angular', assistantId: 'c-messages', urlKey: 'streaming', field: 'streamingAssistantId' }, + { dir: 'cockpit/chat/input/angular', assistantId: 'c-input', urlKey: 'streaming', field: 'streamingAssistantId' }, + { dir: 'cockpit/chat/interrupts/angular', assistantId: 'c-interrupts', urlKey: 'streaming', field: 'streamingAssistantId' }, + { dir: 'cockpit/chat/tool-calls/angular', assistantId: 'c-tool-calls', urlKey: 'streaming', field: 'streamingAssistantId' }, + { dir: 'cockpit/chat/subagents/angular', assistantId: 'c-subagents', urlKey: 'streaming', field: 'streamingAssistantId' }, + { dir: 'cockpit/chat/threads/angular', assistantId: 'c-threads', urlKey: 'streaming', field: 'streamingAssistantId' }, + { dir: 'cockpit/chat/timeline/angular', assistantId: 'c-timeline', urlKey: 'streaming', field: 'streamingAssistantId' }, + { dir: 'cockpit/chat/generative-ui/angular', assistantId: 'c-generative-ui', urlKey: 'streaming', field: 'generativeUiAssistantId' }, + { dir: 'cockpit/chat/debug/angular', assistantId: 'c-debug', urlKey: 'streaming', field: 'streamingAssistantId' }, + { dir: 'cockpit/chat/theming/angular', assistantId: 'c-theming', urlKey: 'streaming', field: 'streamingAssistantId' }, + { dir: 'cockpit/chat/a2ui/angular', assistantId: 'c-a2ui', urlKey: 'streaming', field: 'a2uiAssistantId' }, ]; +function isPendingUrl(url: string): boolean { + return url === 'PENDING_DEPLOYMENT' || url.includes('placeholder'); +} + for (const cap of capabilities) { - const url = urls[cap.assistantId]; - if (!url || url === 'PENDING_DEPLOYMENT') { - console.log(`⏭️ ${cap.assistantId}: skipped (pending deployment)`); + const url = urls[cap.urlKey ?? cap.assistantId]; + if (!url) { + console.log(`⏭️ ${cap.assistantId}: skipped (missing deployment URL)`); + continue; + } + if (isPendingUrl(url)) { + console.log(`⏭️ ${cap.assistantId}: skipped (${url})`); continue; } @@ -40,7 +62,8 @@ for (const cap of capabilities) { * Production environment configuration. * * Points to the LangGraph Cloud deployment managed by LangSmith. - * The assistantId must match the graph name in langgraph.json. + * All active capability files may legitimately share the same deployment URL. + * The assistantId must still match the graph name in langgraph.json. */ export const environment = { production: true, diff --git a/scripts/verify-shared-deployment.ts b/scripts/verify-shared-deployment.ts new file mode 100644 index 000000000..1d8b2e954 --- /dev/null +++ b/scripts/verify-shared-deployment.ts @@ -0,0 +1,204 @@ +#!/usr/bin/env npx tsx +/** + * Verify the shared cockpit LangSmith deployment. + * + * Usage: + * npx tsx scripts/verify-shared-deployment.ts --dry-run + * npx tsx scripts/verify-shared-deployment.ts + */ +import { readFileSync } from 'fs'; +import { resolve } from 'path'; + +type DeploymentUrls = Record; + +const REQUIRED_URL_KEYS = [ + 'streaming', + 'deployment-runtime', + 'planning', + 'filesystem', +] as const; + +const SMOKE_ASSISTANT_IDS = [ + 'streaming', + 'deployment-runtime', + 'planning', + 'filesystem', + 'c-generative-ui', + 'c-a2ui', +] as const; + +const DEPLOYMENT_URLS_PATH = resolve(__dirname, '../deployment-urls.json'); + +function parseArgs(argv: string[]) { + return { + dryRun: argv.includes('--dry-run'), + }; +} + +function readDeploymentUrls(): DeploymentUrls { + const raw = readFileSync(DEPLOYMENT_URLS_PATH, 'utf-8'); + return JSON.parse(raw) as DeploymentUrls; +} + +function normalizeUrl(value: string | undefined): string { + return (value ?? '').trim(); +} + +function isPlaceholderUrl(url: string): boolean { + return url === 'PENDING_DEPLOYMENT' || url.includes('placeholder'); +} + +function getSharedUrl(urls: DeploymentUrls): string { + const entries = Object.entries(urls); + if (entries.length === 0) { + throw new Error('deployment-urls.json does not contain any deployment entries'); + } + + const normalized = entries.map(([name, url]) => [name, normalizeUrl(url)] as const); + const missing = normalized.filter(([, url]) => url.length === 0); + if (missing.length > 0) { + throw new Error(`Missing deployment URL for: ${missing.map(([name]) => name).join(', ')}`); + } + + const unique = [...new Set(normalized.map(([, url]) => url))]; + if (unique.length !== 1) { + throw new Error( + [ + 'deployment-urls.json must resolve every active capability to the same shared URL', + `Found URLs: ${unique.join(', ')}`, + ].join('\n'), + ); + } + + return unique[0]!; +} + +function validateRequiredAssistants(urls: DeploymentUrls) { + const missing = REQUIRED_URL_KEYS.filter((id) => !normalizeUrl(urls[id])); + if (missing.length > 0) { + throw new Error(`Missing required deployment URL keys: ${missing.join(', ')}`); + } +} + +async function fetchJson(url: string, init?: RequestInit) { + const res = await fetch(url, init); + const text = await res.text(); + + if (!res.ok) { + throw new Error(`${res.status} ${res.statusText}${text ? ` - ${text}` : ''}`); + } + + try { + return JSON.parse(text) as unknown; + } catch { + throw new Error(`Expected JSON from ${url}, received: ${text}`); + } +} + +async function verifyHealth(url: string) { + const data = await fetchJson(`${url}/ok`, { + headers: authHeaders(), + signal: AbortSignal.timeout(10000), + }); + + if (!data || typeof data !== 'object' || (data as { ok?: unknown }).ok !== true) { + throw new Error(`/ok returned ${JSON.stringify(data)}`); + } +} + +function authHeaders(): Record { + const apiKey = process.env['LANGSMITH_API_KEY'] ?? ''; + return apiKey ? { 'x-api-key': apiKey } : {}; +} + +async function createThread(url: string) { + const thread = await fetchJson(`${url}/threads`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...authHeaders() }, + body: JSON.stringify({ metadata: {} }), + signal: AbortSignal.timeout(10000), + }); + + const threadId = (thread as { thread_id?: string }).thread_id; + if (!threadId) { + throw new Error(`Thread creation response missing thread_id: ${JSON.stringify(thread)}`); + } + + return threadId; +} + +async function smokeAssistant(url: string, assistantId: string) { + const threadId = await createThread(url); + const runRes = await fetch(`${url}/threads/${threadId}/runs/stream`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...authHeaders() }, + body: JSON.stringify({ + assistant_id: assistantId, + input: { messages: [{ role: 'human', content: 'hello' }] }, + stream_mode: ['values'], + }), + signal: AbortSignal.timeout(30000), + }); + + const text = await runRes.text(); + if (!runRes.ok) { + throw new Error(`${runRes.status} ${runRes.statusText}${text ? ` - ${text}` : ''}`); + } + + if (!text.includes('"type":"ai"')) { + throw new Error(`No AI response in stream: ${text}`); + } +} + +async function main() { + const { dryRun } = parseArgs(process.argv.slice(2)); + const urls = readDeploymentUrls(); + validateRequiredAssistants(urls); + + const sharedUrl = getSharedUrl(urls); + const activeNames = Object.keys(urls).sort(); + + if (dryRun) { + console.log(`✅ deployment-urls.json is shared across ${activeNames.length} active capabilities`); + console.log(`✅ required deployment URL keys present: ${REQUIRED_URL_KEYS.join(', ')}`); + console.log(`✅ smoke assistants configured: ${SMOKE_ASSISTANT_IDS.join(', ')}`); + console.log(`✅ shared URL: ${sharedUrl}`); + if (isPlaceholderUrl(sharedUrl)) { + console.log('ℹ️ shared URL is still a placeholder'); + } + return; + } + + if (isPlaceholderUrl(sharedUrl)) { + throw new Error('deployment-urls.json still points at PENDING_DEPLOYMENT; live verification requires a real URL'); + } + + await verifyHealth(sharedUrl); + console.log(`✅ shared deployment healthy (${sharedUrl})`); + + let passed = 0; + const failures: string[] = []; + + for (const assistantId of SMOKE_ASSISTANT_IDS) { + try { + await smokeAssistant(sharedUrl, assistantId); + console.log(`✅ ${assistantId}: smoke test passed`); + passed++; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + failures.push(`${assistantId}: ${message}`); + console.error(`❌ ${assistantId}: smoke test failed — ${message}`); + } + } + + console.log(`\n${passed} passed, ${failures.length} failed out of ${SMOKE_ASSISTANT_IDS.length}`); + if (failures.length > 0) { + process.exit(1); + } +} + +main().catch((error) => { + const message = error instanceof Error ? error.message : String(error); + console.error(`❌ shared deployment verification failed — ${message}`); + process.exit(1); +});