From 65cce3b287deabcb73d8daaed5927cf5cd8c92b9 Mon Sep 17 00:00:00 2001 From: Maksym Yezhov Date: Tue, 26 May 2026 14:24:44 -0700 Subject: [PATCH] feat: v2 - ai assistant llm proxy and dispatcher agent --- package.json | 3 + pnpm-lock.yaml | 666 ++++++++++++++++++ src/agent/agents/tangleDispatcher.ts | 66 ++ src/agent/aiTokenStore.ts | 20 + src/agent/config.ts | 86 +++ src/agent/middleware/observability.ts | 80 +++ src/agent/prompts/dispatcher.md | 44 ++ src/agent/session.ts | 13 + src/agent/types.ts | 2 + src/agent/worker.ts | 117 ++- src/config/aiAssistantConfig.json | 6 + .../components/AiChat/AiChatContent.tsx | 17 + .../Editor/components/AiChat/agentClient.ts | 10 +- .../Editor/components/AiChat/aiChat.types.ts | 7 + .../AiChat/components/AiTokenSetup.tsx | 53 ++ vite.config.js | 46 +- 16 files changed, 1189 insertions(+), 47 deletions(-) create mode 100644 src/agent/agents/tangleDispatcher.ts create mode 100644 src/agent/aiTokenStore.ts create mode 100644 src/agent/config.ts create mode 100644 src/agent/middleware/observability.ts create mode 100644 src/agent/prompts/dispatcher.md create mode 100644 src/agent/session.ts create mode 100644 src/config/aiAssistantConfig.json create mode 100644 src/routes/v2/pages/Editor/components/AiChat/components/AiTokenSetup.tsx diff --git a/package.json b/package.json index fedfc80c2..5e1e7166e 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,8 @@ "@hyperjump/browser": "^1.3.1", "@hyperjump/json-schema": "^1.17.6", "@monaco-editor/react": "^4.7.0", + "@openai/agents": "^0.4.15", + "@openai/agents-core": "^0.4.15", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.3", @@ -104,6 +106,7 @@ "mobx-keystone": "^1.21.0", "mobx-react-lite": "^4.1.1", "nanoid": "^5.1.11", + "openai": "^6.39.0", "papaparse": "^5.5.3", "prism-react-renderer": "^2.4.1", "pyodide": "^0.29.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6022c0d8f..b9e027981 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,12 @@ importers: '@monaco-editor/react': specifier: ^4.7.0 version: 4.7.0(monaco-editor@0.54.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@openai/agents': + specifier: ^0.4.15 + version: 0.4.15(ws@8.20.1)(zod@4.4.3) + '@openai/agents-core': + specifier: ^0.4.15 + version: 0.4.15(ws@8.20.1)(zod@4.4.3) '@radix-ui/react-alert-dialog': specifier: ^1.1.15 version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -167,6 +173,9 @@ importers: nanoid: specifier: ^5.1.11 version: 5.1.11 + openai: + specifier: ^6.39.0 + version: 6.39.0(ws@8.20.1)(zod@4.4.3) papaparse: specifier: ^5.5.3 version: 5.5.3 @@ -1041,6 +1050,12 @@ packages: '@hey-api/types@0.1.4': resolution: {integrity: sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg==} + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1109,6 +1124,16 @@ packages: resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} engines: {node: '>=8'} + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@monaco-editor/loader@1.7.0': resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==} @@ -1137,6 +1162,29 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@openai/agents-core@0.4.15': + resolution: {integrity: sha512-LDCg7GnKLgAj2Zt5f3Wi40gedez9Oh1zoLA8UgoBGKs2cLwShIMLYoeh8DTE6fHKupzdnRHScAroT3EVTV8JOw==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@openai/agents-openai@0.4.15': + resolution: {integrity: sha512-JCcHyi3IAv3VdUUmnUyQI5EYiAMf9DRsuH3rgxkROdkaHQ2OBVpvwqpRFNMPOKHuLIyZuSPuP/Lf1HcfeXU2zA==} + peerDependencies: + zod: ^4.0.0 + + '@openai/agents-realtime@0.4.15': + resolution: {integrity: sha512-i3eUVCjXFu9AhuhMDZynl+NBaGfpyGRgZ9CYPEvKRlZhxSYnQabtpdDyat7Dv+iljPR1+fzXJcMgci7mQLQFFg==} + peerDependencies: + zod: ^4.0.0 + + '@openai/agents@0.4.15': + resolution: {integrity: sha512-O3CJf8BIA2FoYtUy4cHmFqNcPNi2mjFIcKOySfV8m5BTkZJXElzs6zRWI6TNcEpp4nuJNB/xnKrJz3hSwQI+pw==} + peerDependencies: + zod: ^4.0.0 + '@oxc-parser/binding-android-arm-eabi@0.130.0': resolution: {integrity: sha512-h/xYU8/7ADWzVSf5I+YalLpj33LOy9CI/zgbJNIZ5eunRBG+Czqa3lZsvuPHHf3rOt6z1c5+UzoxjbAzAvhwVw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2402,6 +2450,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@typescript-eslint/eslint-plugin@8.60.0': resolution: {integrity: sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2517,6 +2568,10 @@ packages: '@xyflow/system@0.0.76': resolution: {integrity: sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx-walk@2.0.0: resolution: {integrity: sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==} @@ -2538,9 +2593,20 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -2679,6 +2745,10 @@ packages: peerDependencies: react: '>=17.0.1' + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + brace-expansion@1.1.13: resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} @@ -2713,6 +2783,10 @@ packages: resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} engines: {node: '>=0.10.0'} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + c12@3.3.4: resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} peerDependencies: @@ -2878,19 +2952,39 @@ packages: consola@2.15.3: resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} cookie-es@3.1.1: resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -3023,6 +3117,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dependency-cruiser@17.4.2: resolution: {integrity: sha512-XbrWY2EkIRoqvJL3feRlwHiYrPD2h6iYZ+LaMZgMLpxDBFXKb3+rXGVAli3/fV+yd9rgTBtFpuAYMDt4RjHQUA==} engines: {node: ^20.12||^22||>=24} @@ -3082,6 +3180,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.349: resolution: {integrity: sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==} @@ -3094,6 +3195,10 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -3160,6 +3265,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -3253,10 +3361,32 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.8: + resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express-rate-limit@8.5.2: + resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -3286,6 +3416,9 @@ packages: fast-safe-stringify@1.2.3: resolution: {integrity: sha512-QJYT/i0QYoiZBQ71ivxdyTqkwKkQ0oxACXHYxH2zYHJEgzi2LsbjgvtzTbLi1SZcF190Db2YP7I7eTsU2egOlw==} + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -3317,6 +3450,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-cache-dir@3.3.2: resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} engines: {node: '>=8'} @@ -3363,6 +3500,14 @@ packages: engines: {node: '>=18.3.0'} hasBin: true + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-extra@11.3.4: resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} engines: {node: '>=14.14'} @@ -3534,6 +3679,10 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hono@4.12.23: + resolution: {integrity: sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==} + engines: {node: '>=16.9.0'} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -3547,9 +3696,17 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + hyparquet@1.26.0: resolution: {integrity: sha512-yxUiViPZ+z5h+xdX4rA1G+k30jXoEsG9I2xEpjaM84imGznbKjZzxuZFsdzqg6C4LxNnnAlDFvzpk4uxQWTbTQ==} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + idn-hostname@15.1.8: resolution: {integrity: sha512-MmLwddtSVyMtzYxx+xs2IFEbfyg/facubL/mEaAoJX/XIfjt1ly5QhPByihf4yrxZYbkQfRZVEnBgISv/e2ZWw==} @@ -3598,6 +3755,14 @@ packages: resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} engines: {node: '>=10.13.0'} + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -3722,6 +3887,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -3809,6 +3977,9 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} @@ -3845,6 +4016,12 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -4098,10 +4275,18 @@ packages: mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + meow@6.1.1: resolution: {integrity: sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==} engines: {node: '>=8'} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -4198,10 +4383,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -4272,6 +4465,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + node-exports-info@1.6.0: resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} engines: {node: '>= 0.4'} @@ -4313,6 +4510,10 @@ packages: ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -4324,6 +4525,18 @@ packages: resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} engines: {node: '>=20'} + openai@6.39.0: + resolution: {integrity: sha512-O61LIsimY3acVabwvomwFhwrnN36yvHY2quIfy9keEcFytGgWeV35yLHQ6NVMLSBxRpHmcg2yuhCnlu2HT4pLQ==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -4386,6 +4599,10 @@ packages: parse5@8.0.1: resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4405,6 +4622,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -4437,6 +4657,10 @@ packages: resolution: {integrity: sha512-LFDwmhyWLBnmwO/2UFbWu1jEGVDzaPupaVdx0XcZ3tIAx1EDEBauzxXf2S0UcFK7oe+X9MApjH0hx9U1XMgfCA==} hasBin: true + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} @@ -4500,6 +4724,10 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} @@ -4511,6 +4739,10 @@ packages: resolution: {integrity: sha512-tCseTsqU3kSxZIjkue5zXxTMNEwrKZwOIIEQRBA/VzHxFN1hoCxe4w41phfCdHd9it9RcCNQb5K/Re0InqMgvA==} engines: {node: '>=18.0.0'} + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -4524,6 +4756,14 @@ packages: random-words@2.0.1: resolution: {integrity: sha512-nZNJAmgcFmtJMTDDIUCm/iK4R6RydC6NvALvWhYItXQrgYGk1F7Gww416LpVROFQtfVd5TaLEf4WuSsko03N7w==} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + rc9@3.0.1: resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} @@ -4728,6 +4968,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-applescript@7.1.0: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} @@ -4756,6 +5000,9 @@ packages: safe-regex@2.1.1: resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -4784,6 +5031,10 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + seroval-plugins@1.5.4: resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==} engines: {node: '>=10'} @@ -4794,6 +5045,10 @@ packages: resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==} engines: {node: '>=10'} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -4806,6 +5061,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -4882,6 +5140,10 @@ packages: state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -5048,6 +5310,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tough-cookie@6.0.1: resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} @@ -5111,6 +5377,10 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -5187,6 +5457,10 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + unplugin@2.1.0: resolution: {integrity: sha512-us4j03/499KhbGP8BU7Hrzrgseo+KdfJYWcbcajCOqsAyb8Gk0Yn2kiUIcZISYCb1JFaZfIuG3b42HmguVOKCQ==} engines: {node: '>=18.12.0'} @@ -5235,6 +5509,10 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -5482,6 +5760,11 @@ packages: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + zod-validation-error@3.5.4: resolution: {integrity: sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==} engines: {node: '>=18.0.0'} @@ -6163,6 +6446,11 @@ snapshots: '@hey-api/types@0.1.4': {} + '@hono/node-server@1.19.14(hono@4.12.23)': + dependencies: + hono: 4.12.23 + optional: true + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -6238,6 +6526,29 @@ snapshots: '@lukeed/ms@2.0.2': {} + '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.23) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.8 + express: 5.2.1 + express-rate-limit: 8.5.2(express@5.2.1) + hono: 4.12.23 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + optional: true + '@monaco-editor/loader@1.7.0': dependencies: state-local: 1.0.7 @@ -6268,6 +6579,57 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@openai/agents-core@0.4.15(ws@8.20.1)(zod@4.4.3)': + dependencies: + debug: 4.4.3 + openai: 6.39.0(ws@8.20.1)(zod@4.4.3) + optionalDependencies: + '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) + zod: 4.4.3 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + - ws + + '@openai/agents-openai@0.4.15(ws@8.20.1)(zod@4.4.3)': + dependencies: + '@openai/agents-core': 0.4.15(ws@8.20.1)(zod@4.4.3) + debug: 4.4.3 + openai: 6.39.0(ws@8.20.1)(zod@4.4.3) + zod: 4.4.3 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + - ws + + '@openai/agents-realtime@0.4.15(zod@4.4.3)': + dependencies: + '@openai/agents-core': 0.4.15(ws@8.20.1)(zod@4.4.3) + '@types/ws': 8.18.1 + debug: 4.4.3 + ws: 8.20.1 + zod: 4.4.3 + transitivePeerDependencies: + - '@cfworker/json-schema' + - bufferutil + - supports-color + - utf-8-validate + + '@openai/agents@0.4.15(ws@8.20.1)(zod@4.4.3)': + dependencies: + '@openai/agents-core': 0.4.15(ws@8.20.1)(zod@4.4.3) + '@openai/agents-openai': 0.4.15(ws@8.20.1)(zod@4.4.3) + '@openai/agents-realtime': 0.4.15(zod@4.4.3) + debug: 4.4.3 + openai: 6.39.0(ws@8.20.1)(zod@4.4.3) + zod: 4.4.3 + transitivePeerDependencies: + - '@cfworker/json-schema' + - bufferutil + - supports-color + - utf-8-validate + - ws + '@oxc-parser/binding-android-arm-eabi@0.130.0': optional: true @@ -7365,6 +7727,10 @@ snapshots: '@types/unist@3.0.3': {} + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.9.1 + '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -7554,6 +7920,12 @@ snapshots: d3-selection: 3.0.0 d3-zoom: 3.0.0 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + optional: true + acorn-jsx-walk@2.0.0: {} acorn-jsx@5.3.2(acorn@8.16.0): @@ -7570,6 +7942,11 @@ snapshots: acorn@8.16.0: {} + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + optional: true + ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 @@ -7577,6 +7954,14 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + optional: true + ansi-colors@4.1.3: {} ansi-regex@5.0.1: {} @@ -7714,6 +8099,21 @@ snapshots: dependencies: react: 19.2.6 + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + optional: true + brace-expansion@1.1.13: dependencies: balanced-match: 1.0.2 @@ -7757,6 +8157,9 @@ snapshots: byline@5.0.0: {} + bytes@3.1.2: + optional: true + c12@3.3.4(magicast@0.3.5): dependencies: chokidar: 5.0.0 @@ -7930,14 +8333,32 @@ snapshots: consola@2.15.3: {} + content-disposition@1.1.0: + optional: true + content-type@1.0.5: {} + content-type@2.0.0: + optional: true + convert-source-map@2.0.0: {} cookie-es@3.1.1: {} + cookie-signature@1.2.2: + optional: true + + cookie@0.7.2: + optional: true + core-util-is@1.0.3: {} + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + optional: true + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -8064,6 +8485,9 @@ snapshots: delayed-stream@1.0.0: {} + depd@2.0.0: + optional: true + dependency-cruiser@17.4.2: dependencies: acorn: 8.16.0 @@ -8128,6 +8552,9 @@ snapshots: eastasianwidth@0.2.0: {} + ee-first@1.1.1: + optional: true + electron-to-chromium@1.5.349: {} email-addresses@5.0.0: {} @@ -8136,6 +8563,9 @@ snapshots: emoji-regex@9.2.2: {} + encodeurl@2.0.0: + optional: true + end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -8318,6 +8748,9 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: + optional: true + escape-string-regexp@1.0.5: {} escape-string-regexp@4.0.0: {} @@ -8447,8 +8880,59 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: + optional: true + + eventsource-parser@3.0.8: + optional: true + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.8 + optional: true + expect-type@1.3.0: {} + express-rate-limit@8.5.2(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.2.0 + optional: true + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.2 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + optional: true + exsolve@1.0.8: {} extend@3.0.2: {} @@ -8473,6 +8957,9 @@ snapshots: fast-safe-stringify@1.2.3: {} + fast-uri@3.1.2: + optional: true + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -8501,6 +8988,18 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + optional: true + find-cache-dir@3.3.2: dependencies: commondir: 1.0.1 @@ -8553,6 +9052,12 @@ snapshots: dependencies: fd-package-json: 2.0.0 + forwarded@0.2.0: + optional: true + + fresh@2.0.0: + optional: true + fs-extra@11.3.4: dependencies: graceful-fs: 4.2.11 @@ -8746,6 +9251,9 @@ snapshots: dependencies: hermes-estree: 0.25.1 + hono@4.12.23: + optional: true + hosted-git-info@2.8.9: {} html-encoding-sniffer@6.0.0: @@ -8758,8 +9266,22 @@ snapshots: html-url-attributes@3.0.1: {} + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + optional: true + hyparquet@1.26.0: {} + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + optional: true + idn-hostname@15.1.8: dependencies: punycode: 2.3.1 @@ -8798,6 +9320,12 @@ snapshots: interpret@3.1.1: {} + ip-address@10.2.0: + optional: true + + ipaddr.js@1.9.1: + optional: true + is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -8909,6 +9437,9 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: + optional: true + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -9002,6 +9533,9 @@ snapshots: jiti@2.7.0: {} + jose@6.2.3: + optional: true + js-tokens@10.0.0: {} js-tokens@4.0.0: {} @@ -9046,6 +9580,12 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: + optional: true + + json-schema-typed@8.0.2: + optional: true + json-stable-stringify-without-jsonify@1.0.1: {} json-stringify-deterministic@1.0.13: {} @@ -9383,6 +9923,9 @@ snapshots: mdn-data@2.27.1: {} + media-typer@1.1.0: + optional: true + meow@6.1.1: dependencies: '@types/minimist': 1.2.5 @@ -9397,6 +9940,9 @@ snapshots: type-fest: 0.13.1 yargs-parser: 18.1.3 + merge-descriptors@2.0.0: + optional: true + merge2@1.4.1: {} micromark-core-commonmark@2.0.3: @@ -9597,10 +10143,18 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: + optional: true + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + optional: true + mimic-function@5.0.1: {} min-indent@1.0.1: {} @@ -9658,6 +10212,9 @@ snapshots: natural-compare@1.4.0: {} + negotiator@1.0.0: + optional: true + node-exports-info@1.6.0: dependencies: array.prototype.flatmap: 1.3.3 @@ -9712,6 +10269,11 @@ snapshots: ohash@2.0.11: {} + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + optional: true + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -9729,6 +10291,11 @@ snapshots: powershell-utils: 0.1.0 wsl-utils: 0.3.1 + openai@6.39.0(ws@8.20.1)(zod@4.4.3): + optionalDependencies: + ws: 8.20.1 + zod: 4.4.3 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -9855,6 +10422,9 @@ snapshots: dependencies: entities: 8.0.0 + parseurl@1.3.3: + optional: true + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -9868,6 +10438,9 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.3 + path-to-regexp@8.4.2: + optional: true + path-type@4.0.0: {} pathe@2.0.3: {} @@ -9895,6 +10468,9 @@ snapshots: quick-format-unescaped: 1.1.2 split2: 2.2.0 + pkce-challenge@5.0.1: + optional: true + pkg-dir@4.2.0: dependencies: find-up: 4.1.0 @@ -9956,6 +10532,12 @@ snapshots: property-information@7.1.0: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + optional: true + pump@3.0.4: dependencies: end-of-stream: 1.4.5 @@ -9971,6 +10553,11 @@ snapshots: - bufferutil - utf-8-validate + qs@6.15.2: + dependencies: + side-channel: 1.1.0 + optional: true + queue-microtask@1.2.3: {} quick-format-unescaped@1.1.2: @@ -9983,6 +10570,17 @@ snapshots: dependencies: seedrandom: 3.0.5 + range-parser@1.2.1: + optional: true + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + optional: true + rc9@3.0.1: dependencies: defu: 6.1.7 @@ -10280,6 +10878,17 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.1 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + optional: true + run-applescript@7.1.0: {} run-parallel@1.2.0: @@ -10313,6 +10922,9 @@ snapshots: dependencies: regexp-tree: 0.1.27 + safer-buffer@2.1.2: + optional: true + saxes@6.0.0: dependencies: xmlchars: 2.2.0 @@ -10329,12 +10941,39 @@ snapshots: semver@7.8.1: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + optional: true + seroval-plugins@1.5.4(seroval@1.5.4): dependencies: seroval: 1.5.4 seroval@1.5.4: {} + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + optional: true + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -10357,6 +10996,9 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + setprototypeof@1.2.0: + optional: true + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -10433,6 +11075,9 @@ snapshots: state-local@1.0.7: {} + statuses@2.0.2: + optional: true + std-env@3.10.0: {} stdin-discarder@0.3.2: {} @@ -10617,6 +11262,9 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: + optional: true + tough-cookie@6.0.1: dependencies: tldts: 7.0.29 @@ -10672,6 +11320,13 @@ snapshots: type-fest@0.8.1: {} + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.0 + mime-types: 3.0.2 + optional: true + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -10772,6 +11427,9 @@ snapshots: universalify@2.0.1: {} + unpipe@1.0.0: + optional: true + unplugin@2.1.0: dependencies: acorn: 8.16.0 @@ -10816,6 +11474,9 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + vary@1.1.2: + optional: true + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -11053,6 +11714,11 @@ snapshots: yoctocolors@2.1.2: {} + zod-to-json-schema@3.25.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + optional: true + zod-validation-error@3.5.4(zod@3.25.76): dependencies: zod: 3.25.76 diff --git a/src/agent/agents/tangleDispatcher.ts b/src/agent/agents/tangleDispatcher.ts new file mode 100644 index 000000000..71611ad6f --- /dev/null +++ b/src/agent/agents/tangleDispatcher.ts @@ -0,0 +1,66 @@ +/** + * Top-level dispatcher agent for the in-browser AI assistant. + */ +import { Agent, MemorySession, run } from "@openai/agents"; + +import { ensureProxyConfigured, requireOrchestratorModel } from "../config"; +import { attachObservabilityHooks } from "../middleware/observability"; +import dispatcherPrompt from "../prompts/dispatcher.md?raw"; +import type { AgentSession } from "../session"; +import type { StatusCallback } from "../types"; + +interface DispatcherInvokeParams { + message: string; + threadId: string; + token: string; + session: AgentSession; +} + +interface DispatcherInvokeResult { + answer: string; + threadId: string; +} + +export interface TangleDispatcher { + invoke(params: DispatcherInvokeParams): Promise; +} + +export function createDispatcher({ + emitStatus, +}: { + emitStatus: StatusCallback; +}): TangleDispatcher { + const sessions = new Map(); + + const agent = Agent.create({ + name: "tangle-dispatcher", + model: requireOrchestratorModel(), + instructions: dispatcherPrompt, + tools: [], + handoffs: [], + }); + attachObservabilityHooks(agent, emitStatus); + + function getOrCreateSessionMemory(threadId: string): MemorySession { + const existing = sessions.get(threadId); + if (existing) return existing; + const created = new MemorySession({ sessionId: threadId }); + sessions.set(threadId, created); + return created; + } + + return { + async invoke(params) { + ensureProxyConfigured(params.token); + const sessionMemory = getOrCreateSessionMemory(params.threadId); + const result = await run(agent, params.message, { + session: sessionMemory, + }); + const answer = + typeof result.finalOutput === "string" + ? result.finalOutput + : JSON.stringify(result.finalOutput ?? ""); + return { answer, threadId: params.threadId }; + }, + }; +} diff --git a/src/agent/aiTokenStore.ts b/src/agent/aiTokenStore.ts new file mode 100644 index 000000000..2ff0d8f12 --- /dev/null +++ b/src/agent/aiTokenStore.ts @@ -0,0 +1,20 @@ +/** + * Shared IndexedDB-backed storage for the AI assistant's LLM proxy token. + */ +import localforage from "localforage"; + +const store = localforage.createInstance({ + name: "oasis-app", + storeName: "settings", + description: "Store for application settings", +}); + +const KEY = "aiAssistantProxyToken"; + +export async function getAiToken(): Promise { + return (await store.getItem(KEY)) ?? null; +} + +export async function setAiToken(token: string): Promise { + await store.setItem(KEY, token); +} diff --git a/src/agent/config.ts b/src/agent/config.ts new file mode 100644 index 000000000..63e1d861c --- /dev/null +++ b/src/agent/config.ts @@ -0,0 +1,86 @@ +/** + * Configuration and one-time setup for the in-browser agent. + * + * Static values (proxy URL, model names, mode) come from the committed + * `src/config/aiAssistantConfig.json` file. The secret proxy token is + * NOT read here — the worker reads it from IndexedDB (via + * `src/agent/aiTokenStore.ts`, shared with the main thread) on every + * `ask()` turn and passes the resolved value into + * `ensureProxyConfigured(token)`, which re-builds the OpenAI client + * when the token rotates. + * + * `proxyMode` exists so a future PR can flip the runtime from + * `"browser-direct"` (current beta) to `"backend-proxy"` without a + * rewrite. Only `"browser-direct"` is implemented in this PR. + */ +import { + setDefaultOpenAIClient, + setOpenAIAPI, + setTracingDisabled, +} from "@openai/agents"; +import OpenAI from "openai"; + +import aiAssistantConfig from "@/config/aiAssistantConfig.json"; + +export const config = aiAssistantConfig as { + proxyBaseUrl: string; + proxyMode: "browser-direct" | "backend-proxy"; + orchestratorModel: string; + subagentModel: string; +}; + +function requireProxyBaseUrl(): string { + if (!config.proxyBaseUrl) { + throw new Error( + "AI assistant: proxyBaseUrl is empty. Set it in src/config/aiAssistantConfig.json before enabling the ai-assistant beta flag.", + ); + } + return config.proxyBaseUrl; +} + +export function requireOrchestratorModel(): string { + if (!config.orchestratorModel) { + throw new Error( + "AI assistant: orchestratorModel is empty. Set it in src/config/aiAssistantConfig.json.", + ); + } + return config.orchestratorModel; +} + +let lastConfiguredToken: string | null = null; + +/** + * Wires the configured LLM proxy as the default OpenAI client for + * `@openai/agents`. Called once per turn from the dispatcher with the + * current token. Re-builds the client when the token rotates; otherwise + * a no-op. + * + * - `setOpenAIAPI("chat_completions")`: the proxy exposes Chat + * Completions, not the OpenAI Responses API. + * - `setTracingDisabled(true)`: the SDK's default tracing exporter + * would POST to `api.openai.com`, which is unreachable through the + * proxy. + */ +export function ensureProxyConfigured(token: string): void { + if (config.proxyMode === "backend-proxy") { + throw new Error( + "AI assistant: backend-proxy mode is not implemented yet. Set proxyMode to 'browser-direct' in src/config/aiAssistantConfig.json.", + ); + } + if (!token) { + throw new Error( + "AI assistant: missing proxy token. Set it via the AI panel.", + ); + } + if (lastConfiguredToken === token) return; + setDefaultOpenAIClient( + new OpenAI({ + apiKey: token, + baseURL: requireProxyBaseUrl(), + dangerouslyAllowBrowser: true, + }), + ); + setOpenAIAPI("chat_completions"); + setTracingDisabled(true); + lastConfiguredToken = token; +} diff --git a/src/agent/middleware/observability.ts b/src/agent/middleware/observability.ts new file mode 100644 index 000000000..db98f75ae --- /dev/null +++ b/src/agent/middleware/observability.ts @@ -0,0 +1,80 @@ +/** + * Observability hooks for the in-browser agent. + * + * `@openai/agents` exposes lifecycle events on every `Agent` instance + * via its inherited `EventEmitter`. We attach listeners that translate + * the raw events into short status strings and forward them to the + * main thread through the Comlink-proxied status callback. + * + * Wire this on EVERY agent. Once an agent is active after a handoff, only its + * own hooks fire, not the dispatcher's. Without per-agent wiring the + * status line freezes mid-conversation. + */ +import type { Agent } from "@openai/agents"; + +import type { StatusCallback } from "../types"; + +const TOOL_STATUS_LABELS: Record = { + search_components: "Searching component registry...", + search_docs: "Searching documentation...", + get_pipeline_state: "Reading pipeline state...", + add_task: "Adding task...", + delete_task: "Removing task...", + rename_task: "Renaming task...", + add_input: "Adding input...", + add_output: "Adding output...", + delete_input: "Removing input...", + delete_output: "Removing output...", + connect_nodes: "Connecting nodes...", + delete_edge: "Removing connection...", + set_task_argument: "Configuring task...", + create_subgraph: "Creating subgraph...", + unpack_subgraph: "Unpacking subgraph...", + validate_pipeline: "Validating pipeline...", + submit_pipeline_run: "Submitting run...", + get_run_status: "Checking run status...", + debug_pipeline_run: "Fetching run logs...", + get_pipeline_run: "Fetching run details...", + get_execution_state: "Inspecting execution state...", + get_execution_details: "Fetching execution details...", + get_container_state: "Inspecting container state...", + get_container_log: "Fetching container logs...", +}; + +const SUB_AGENT_LABELS: Record = { + "pipeline-architect": "Building pipeline...", + "pipeline-repair": "Repairing pipeline...", + "debug-assistant": "Analyzing issues...", + "general-help": "Looking up information...", +}; + +// `Agent` matches both the dispatcher (which infers handoff +// output types) and each sub-agent (default `TextOutput`). The hook +// payloads are independent of the agent's generic parameters, so the +// looser type here is intentional. +export function attachObservabilityHooks( + agent: Agent, + emitStatus: StatusCallback, +): void { + agent.on("agent_start", () => { + emitStatus({ text: "Thinking..." }); + }); + + agent.on("agent_end", () => { + emitStatus({ text: "Preparing response..." }); + }); + + agent.on("agent_tool_start", (_ctx, toolDef) => { + emitStatus({ + text: TOOL_STATUS_LABELS[toolDef.name] ?? "Working...", + }); + }); + + agent.on("agent_handoff", (_ctx, nextAgent) => { + emitStatus({ + text: + SUB_AGENT_LABELS[nextAgent.name] ?? + `Delegating to ${nextAgent.name}...`, + }); + }); +} diff --git a/src/agent/prompts/dispatcher.md b/src/agent/prompts/dispatcher.md new file mode 100644 index 000000000..4c5b8930f --- /dev/null +++ b/src/agent/prompts/dispatcher.md @@ -0,0 +1,44 @@ +# Tangle Assistant — System Prompt + +You are the **Tangle Assistant**, an AI helper for Tangle Pipeline Studio. Your job in this release is to answer questions about Tangle, ML pipelines, and how to use the product. + +## What you can do today + +- Explain Tangle concepts (pipelines, tasks, components, runs, executions, inputs/outputs, subgraphs, etc.). +- Discuss ML pipeline patterns and best practices at a general level. +- Suggest approaches the user could take in Tangle Pipeline Studio. + +## What you cannot do today + +- You **cannot** inspect the user's current pipeline. You have no access to its tasks, connections, arguments, or YAML. +- You **cannot** make changes to the pipeline. You have no tools that mutate the editor state. +- You **cannot** run pipelines, fetch run logs, or look up execution status. + +If the user asks for any of the above, be upfront: explain that those abilities are not available yet, and offer what you can — for example, a conceptual explanation, a checklist, or pseudocode they can apply themselves. + +## Off-topic handling + +If the user asks something unrelated to Tangle, ML pipelines, or data workflows, respond briefly and politely: + +> "I'm the Tangle Assistant — I can help with pipeline concepts and how to use Tangle. That question is outside what I can help with today." + +Do not attempt to answer off-topic questions. + +## Response formatting + +Future releases will surface entities and components as interactive chips in the chat panel via these markdown link formats: + +``` +[Entity Name](entity://$id) +[Component Name](component://component-id) +``` + +If you ever reference a specific entity or component by id, use those link formats verbatim — do not rewrite them as bold, italic, or backticks. Today you do not have a tool to look up real ids, so only emit these links when the user has already mentioned an id explicitly. + +## Style + +- Be brief and natural. Aim for a few short paragraphs or a short list, not a wall of text. +- Use plain language. Define jargon when you introduce it. +- When you give steps, number them. +- When code is helpful, use fenced code blocks with an explicit language tag. +- Never apologize for limitations more than once per turn — state them plainly and move on. diff --git a/src/agent/session.ts b/src/agent/session.ts new file mode 100644 index 000000000..54f0c130a --- /dev/null +++ b/src/agent/session.ts @@ -0,0 +1,13 @@ +/** + * Per-request session for the in-browser agent. + */ + +export interface AgentSession { + threadId: string; +} + +export function createSession(params: { threadId: string }): AgentSession { + return { + threadId: params.threadId, + }; +} diff --git a/src/agent/types.ts b/src/agent/types.ts index a41daa266..da3803818 100644 --- a/src/agent/types.ts +++ b/src/agent/types.ts @@ -7,3 +7,5 @@ export interface AgentResponse { threadId: string; componentReferences: Record; } + +export type StatusCallback = (status: { text: string }) => void; diff --git a/src/agent/worker.ts b/src/agent/worker.ts index 031c55cee..142ed5f4c 100644 --- a/src/agent/worker.ts +++ b/src/agent/worker.ts @@ -1,15 +1,18 @@ /** * Web Worker entry point for the in-browser agent. * - * A placeholder `ask()` that echoes the user's message — it - * proves the bundling, lazy-spawn, and Comlink round-trip are working - * end-to-end before we wire the LLM and the tool bridge. + * Main-thread `AgentClient` spawns this worker once (lazily) and + * `init(onStatus)` is called immediately after spawn. * - * The `globalThis.process` stub below covers an unguarded - * `process.env.X` read in `@openai/agents-core` v0.4.x - * (`runner/sessionPersistence.mjs` reads + * Worker-only resolution of `@openai/agents-core/_shims` to its + * browser variant is handled by the `worker.plugins` block in + * `vite.config.js`. The runtime side of "no Node in the worker" — the + * unguarded `process.env.X` read in `@openai/agents-core` v0.4.x — + * is handled by the `globalThis.process` stub below. + * + * The stub covers `runner/sessionPersistence.mjs` reading * `process.env.OPENAI_AGENTS__DEBUG_SAVE_SESSION` without a - * `typeof process` guard) that would otherwise throw + * `typeof process` guard, which would otherwise throw * `ReferenceError: process is not defined` on every turn that * persists session state. The stub deliberately omits `.on` / * `.exit` so the SDK's `typeof process.on === 'function'` checks @@ -35,7 +38,15 @@ if (!Object.prototype.hasOwnProperty.call(globalThis, "process")) { import * as Comlink from "comlink"; -import type { AgentResponse } from "./types"; +import type { ComponentRefData } from "@/routes/v2/pages/Editor/components/AiChat/aiChat.types"; + +import { + createDispatcher, + type TangleDispatcher, +} from "./agents/tangleDispatcher"; +import { getAiToken } from "./aiTokenStore"; +import { createSession } from "./session"; +import type { AgentResponse, StatusCallback } from "./types"; export interface AskParams { message: string; @@ -43,44 +54,78 @@ export interface AskParams { } export interface AgentWorkerApi { - ask(params: AskParams): Promise; + init(onStatus: StatusCallback): void; ping(): Promise<"pong">; + ask(params: AskParams): Promise; } -let emitStatus: (status: { text: string }) => void = () => {}; - function generateThreadId(): string { return `thread-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; } -const api: AgentWorkerApi = { - async ping() { - return "pong"; - }, +function createWorkerApi(): AgentWorkerApi { + let dispatcher: TangleDispatcher | null = null; - async ask({ message, threadId }) { + return { /** - * todo: replace with actual AI response. + * Initialization entry point. Called once by the main thread + * immediately after spawning the worker. Splits init from ask() so + * the tool bridge plumbing and skill warm-up have an explicit + * lifecycle hook later. */ - return { - answer: `Worker echo: ${message}`, - threadId: threadId ?? generateThreadId(), - componentReferences: {}, - }; - }, -}; + init(onStatus) { + if (dispatcher) return; + dispatcher = createDispatcher({ emitStatus: onStatus }); + }, -/** - * Initialization entry point. Called once by the main thread immediately - * after spawning the worker. Splits init from ask() so future bridge / - * skill plumbing has an explicit lifecycle hook. - */ -export function init(onStatus: (status: { text: string }) => void): void { - emitStatus = onStatus; - // TODO: read `emitStatus` from the dispatcher's observability hooks; - // until then this no-op read keeps `noUnusedLocals` quiet without - // dropping the assignment that locks in the init() contract. - void emitStatus; + async ping() { + return "pong"; + }, + + async ask({ message, threadId }) { + if (!dispatcher) { + throw new Error( + "Agent worker not initialized. Call init() before ask().", + ); + } + const token = await getAiToken(); + if (!token) { + throw new Error( + "AI assistant token is missing. Open the AI panel to set it.", + ); + } + const resolvedThreadId = threadId ?? generateThreadId(); + const session = createSession({ + threadId: resolvedThreadId, + }); + + const result = await dispatcher.invoke({ + message, + threadId: resolvedThreadId, + token, + session, + }); + + // TODO: replace this empty map with `session.componentReferences` + // populated by the search_components tool. + const componentReferences: AgentResponse["componentReferences"] = {}; + const refs = new Map(); + for (const [id, ref] of refs) { + if (result.answer.includes(`component://${id}`)) { + componentReferences[id] = { + name: ref.name, + yamlText: ref.yamlText, + }; + } + } + + return { + answer: result.answer, + threadId: result.threadId, + componentReferences, + }; + }, + }; } -Comlink.expose({ ...api, init }); +Comlink.expose(createWorkerApi()); diff --git a/src/config/aiAssistantConfig.json b/src/config/aiAssistantConfig.json new file mode 100644 index 000000000..852c407fa --- /dev/null +++ b/src/config/aiAssistantConfig.json @@ -0,0 +1,6 @@ +{ + "proxyBaseUrl": "", + "proxyMode": "browser-direct", + "orchestratorModel": "", + "subagentModel": "" +} diff --git a/src/routes/v2/pages/Editor/components/AiChat/AiChatContent.tsx b/src/routes/v2/pages/Editor/components/AiChat/AiChatContent.tsx index e0fc21fbb..7a66debac 100644 --- a/src/routes/v2/pages/Editor/components/AiChat/AiChatContent.tsx +++ b/src/routes/v2/pages/Editor/components/AiChat/AiChatContent.tsx @@ -1,15 +1,25 @@ +import { useQuery } from "@tanstack/react-query"; import { observer } from "mobx-react-lite"; +import { getAiToken } from "@/agent/aiTokenStore"; +import { config } from "@/agent/config"; import { BlockStack } from "@/components/ui/layout"; import useToastNotification from "@/hooks/useToastNotification"; +import { AiAssistantTokenQueryKeys } from "./aiChat.types"; import { useAiChatStore } from "./AiChatStoreContext"; +import { AiTokenSetup } from "./components/AiTokenSetup"; import { ChatInput } from "./components/ChatInput"; import { ChatMessageList } from "./components/ChatMessageList"; export const AiChatContent = observer(function AiChatContent() { const aiChat = useAiChatStore(); const notify = useToastNotification(); + const { data: token, isPending: isTokenLoading } = useQuery({ + queryKey: AiAssistantTokenQueryKeys.Token(), + queryFn: getAiToken, + staleTime: Infinity, + }); function handleSend(prompt: string) { aiChat.sendMessage(prompt, { @@ -17,6 +27,13 @@ export const AiChatContent = observer(function AiChatContent() { }); } + if (isTokenLoading) return null; + + const needsToken = token == null && config.proxyMode === "browser-direct"; + if (needsToken) { + return ; + } + return ( void): void; -} - interface InitDeps { onStatus: (status: { text: string }) => void; } @@ -25,18 +21,18 @@ interface AskOptions { class AgentClient { private worker: Worker | null = null; - private remote: Comlink.Remote | null = null; + private remote: Comlink.Remote | null = null; private initPromise: Promise | null = null; private async ensureInit( deps: InitDeps, - ): Promise> { + ): Promise> { if (!this.worker) { this.worker = new Worker(new URL("@/agent/worker.ts", import.meta.url), { type: "module", name: "tangle-agent", }); - this.remote = Comlink.wrap(this.worker); + this.remote = Comlink.wrap(this.worker); } if (!this.remote) { throw new Error("Worker remote was not created"); diff --git a/src/routes/v2/pages/Editor/components/AiChat/aiChat.types.ts b/src/routes/v2/pages/Editor/components/AiChat/aiChat.types.ts index 6d5ac751e..a43539fc3 100644 --- a/src/routes/v2/pages/Editor/components/AiChat/aiChat.types.ts +++ b/src/routes/v2/pages/Editor/components/AiChat/aiChat.types.ts @@ -9,3 +9,10 @@ export interface ChatMessage { content: string; componentReferences?: Record; } + +/** + * Query keys for React Query. + */ +export const AiAssistantTokenQueryKeys = { + Token: () => ["ai-assistant-token"] as const, +} as const; diff --git a/src/routes/v2/pages/Editor/components/AiChat/components/AiTokenSetup.tsx b/src/routes/v2/pages/Editor/components/AiChat/components/AiTokenSetup.tsx new file mode 100644 index 000000000..587c9f803 --- /dev/null +++ b/src/routes/v2/pages/Editor/components/AiChat/components/AiTokenSetup.tsx @@ -0,0 +1,53 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; + +import { setAiToken } from "@/agent/aiTokenStore"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { BlockStack } from "@/components/ui/layout"; +import { Text } from "@/components/ui/typography"; +import { AiAssistantTokenQueryKeys } from "@/routes/v2/pages/Editor/components/AiChat/aiChat.types"; + +export function AiTokenSetup() { + const [value, setValue] = useState(""); + const queryClient = useQueryClient(); + const { mutate: setAiTokenMutation, isPending: isSettingAiToken } = + useMutation({ + mutationFn: setAiToken, + onSuccess: (_data: void, token: string) => { + queryClient.setQueryData(AiAssistantTokenQueryKeys.Token(), token); + setValue(""); + }, + }); + const trimmed = value.trim(); + const canSave = trimmed.length > 0 && !isSettingAiToken; + + function handleSave() { + if (!canSave) return; + setAiTokenMutation(trimmed); + } + + return ( + + + Connect the AI assistant + + + Paste your LLM proxy token to enable the assistant. It stays in this + browser only. + + setValue(event.target.value)} + onEnter={handleSave} + /> + + + ); +} diff --git a/vite.config.js b/vite.config.js index 84e478850..90434dc2a 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,5 +1,6 @@ import tailwindcss from "@tailwindcss/vite"; import viteReact from "@vitejs/plugin-react"; +import { createRequire } from "module"; import path from "path"; import { fileURLToPath } from "url"; import { defineConfig, loadEnv } from "vite"; @@ -11,6 +12,18 @@ import { REACT_COMPILER_ENABLED_DIRS } from "./react-compiler.config.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +const require = createRequire(import.meta.url); + +// `@openai/agents-core` only exposes `.` and `./_shims` in its +// `exports` map — the raw `dist/shims/shims-browser.mjs` subpath is +// not exported, so `require.resolve` on it fails. Resolve the public +// root entry and walk to the sibling browser shim file inside the +// package. +const agentsCoreBrowserShim = path.resolve( + path.dirname(require.resolve("@openai/agents-core")), + "shims/shims-browser.mjs", +); + export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ""); @@ -74,12 +87,37 @@ export default defineConfig(({ mode }) => { }, }, assetsInclude: ["**/*.yaml", "**/*.py"], - // The agent runs in a Web Worker. The `@openai/agents-core/_shims` - // alias and matching plugin land in PR 3 alongside the SDK - // dependency itself; PR 2 only needs ESM output for the worker - // bundle so dynamic `import.meta.url` resolves correctly. + // The agent runs in a Web Worker. `@openai/agents-core/_shims` + // exposes a `browser` export condition, but Vite's worker bundle + // does not reliably apply it — the catch-all condition falls + // through to the Node shim that imports `node:process`. We force + // the browser variant via a scoped `resolveId`, kept here so the + // main bundle (which already resolves correctly via export + // conditions) is not affected. + // + // The runtime side of "no Node in the worker" — specifically the + // unguarded `process.env.X` read in `@openai/agents-core` — is + // handled by the `globalThis.process` stub at the top of + // `src/agent/worker.ts`, not here, so the fix applies in both + // `vite build` and `vite serve` (dev) modes. + // + // `debug` (transitive of `@openai/agents-core`) is handled + // automatically by its package.json `browser` field, which Vite + // does honor for the worker bundle. worker: { format: "es", + plugins: () => [ + { + name: "tangle-agent-worker-shims", + enforce: "pre", + resolveId(id) { + if (id === "@openai/agents-core/_shims") { + return agentsCoreBrowserShim; + } + return null; + }, + }, + ], }, test: { globals: true,