From 3f5e113663501dd3f4ae903c4d835ca26aaa5b8e Mon Sep 17 00:00:00 2001 From: RogierKonings Date: Wed, 29 Jul 2026 15:16:43 +0200 Subject: [PATCH 01/10] feat(core): add secure local Pi app-chat harness --- .changeset/local-pi-app-chat.md | 5 + packages/core/docs/content/harness-agents.mdx | 56 ++- .../content/locales/ar-SA/harness-agents.mdx | 16 +- .../content/locales/de-DE/harness-agents.mdx | 16 +- .../content/locales/es-ES/harness-agents.mdx | 16 +- .../content/locales/fr-FR/harness-agents.mdx | 16 +- .../content/locales/hi-IN/harness-agents.mdx | 16 +- .../content/locales/ja-JP/harness-agents.mdx | 16 +- .../content/locales/ko-KR/harness-agents.mdx | 16 +- .../content/locales/pt-BR/harness-agents.mdx | 16 +- .../content/locales/zh-CN/harness-agents.mdx | 16 +- .../content/locales/zh-TW/harness-agents.mdx | 16 +- .../src/agent/harness/ai-sdk-adapter.spec.ts | 52 +++ .../core/src/agent/harness/ai-sdk-adapter.ts | 61 ++- packages/core/src/agent/harness/builtin.ts | 17 + .../core/src/agent/harness/chat-tools.spec.ts | 97 +++++ packages/core/src/agent/harness/chat-tools.ts | 104 +++++ packages/core/src/agent/harness/index.ts | 20 + .../agent/harness/local-pi-adapter.spec.ts | 106 +++++ .../src/agent/harness/local-pi-adapter.ts | 367 ++++++++++++++++ .../core/src/agent/harness/runner.spec.ts | 58 ++- packages/core/src/agent/harness/runner.ts | 39 ++ packages/core/src/agent/harness/store.ts | 45 +- .../core/src/agent/harness/translate.spec.ts | 26 +- packages/core/src/agent/harness/translate.ts | 10 +- packages/core/src/agent/harness/types.ts | 20 +- .../server/agent-chat-plugin.surface.spec.ts | 17 + packages/core/src/server/agent-chat-plugin.ts | 80 +++- .../server/agent-chat/harness-handler.spec.ts | 329 ++++++++++++++ .../src/server/agent-chat/harness-handler.ts | 406 ++++++++++++++++++ .../src/server/agent-chat/plugin-options.ts | 8 + packages/core/src/server/index.ts | 1 + 32 files changed, 1972 insertions(+), 112 deletions(-) create mode 100644 .changeset/local-pi-app-chat.md create mode 100644 packages/core/src/agent/harness/chat-tools.spec.ts create mode 100644 packages/core/src/agent/harness/chat-tools.ts create mode 100644 packages/core/src/agent/harness/local-pi-adapter.spec.ts create mode 100644 packages/core/src/agent/harness/local-pi-adapter.ts create mode 100644 packages/core/src/server/agent-chat/harness-handler.spec.ts create mode 100644 packages/core/src/server/agent-chat/harness-handler.ts diff --git a/.changeset/local-pi-app-chat.md b/.changeset/local-pi-app-chat.md new file mode 100644 index 0000000000..87f2df7113 --- /dev/null +++ b/.changeset/local-pi-app-chat.md @@ -0,0 +1,5 @@ +--- +"@agent-native/core": minor +--- + +Add an optional full-harness execution path for app chat, including scoped session resume, approval-safe host actions, and a local Pi adapter that reuses Pi-owned configuration without copying OAuth credentials. diff --git a/packages/core/docs/content/harness-agents.mdx b/packages/core/docs/content/harness-agents.mdx index 7289ebf013..11decf3228 100644 --- a/packages/core/docs/content/harness-agents.mdx +++ b/packages/core/docs/content/harness-agents.mdx @@ -92,20 +92,64 @@ spawn background / sub-agent runs with [Custom Agents & Teams](/docs/agent-teams ## Built-in harnesses {#built-in} `registerBuiltinAgentHarnesses()` registers three adapters backed by the AI SDK -`HarnessAgent`: +`HarnessAgent`, plus a direct local Pi adapter: | Name | Runtime | Sandbox | Approvals | | ---------------------------- | ----------- | ------- | --------- | | `ai-sdk-harness:claude-code` | Claude Code | yes | yes | | `ai-sdk-harness:codex` | Codex | yes | no | -| `ai-sdk-harness:pi` | Pi | no | yes | +| `ai-sdk-harness:pi` | Pi | yes | yes | +| `pi:local` | Local Pi | no | no | Their runtime packages are **optional peer dependencies** and load lazily, so an app that never uses a harness does not pay for it. Each adapter carries an -`installPackage` hint (for example `@ai-sdk/harness@canary -@ai-sdk/harness-codex@canary`); `resolveAgentHarness` throws a clear install -error if the packages are missing, and `isAgentHarnessPackageInstalled(entry)` -lets you check first. +`installPackage` hint; `resolveAgentHarness` throws a clear install error if the +packages are missing, and `isAgentHarnessPackageInstalled(entry)` lets you check +first. + +### Local Pi app chat {#local-pi-app-chat} + +`pi:local` runs `@earendil-works/pi-coding-agent` in the app's local Node +process. It uses Pi's normal agent directory (by default `~/.pi/agent`) so Pi +owns model selection and OAuth refresh. Agent-Native never reads, copies, logs, +or stores Pi's auth file; SQL stores only the opaque Pi session-file reference +needed to resume the thread. + +Install the optional runtime package, authenticate Pi once, and configure the +normal agent-chat plugin with an explicit fail-closed local-development guard: + +```bash +pnpm add @earendil-works/pi-coding-agent +pi +``` + +In Pi, run `/login` and choose **OpenAI ChatGPT Plus/Pro (Codex)**. + +```ts +createAgentChatPlugin({ + harness: { + adapter: { name: "pi:local" }, + cwd: process.cwd(), + permissionMode: "allow-edits", + guard: (event) => isTrustedLocalPiRequest(event), + }, +}); +``` + +`isTrustedLocalPiRequest` is deliberately host-owned: require development mode +and a verified loopback/localhost request. A denial must return false (or a +structured 403/503 decision); it never falls back to an API-billed engine. +`pi:local` exposes read-only Pi filesystem tools by default and routes app +operations through the request-filtered Agent-Native action tools. In +`allow-reads` mode, only actions explicitly marked read-only are exposed. The +adapter currently has no native approval continuation, so actions that declare +`needsApproval` fail closed rather than running. Harness app chat is foreground +only, does not accept attachments yet, and is intended for a trusted single-user +local process—not serverless or shared hosting. + +`ai-sdk-harness:pi` is the sandboxed AI SDK adapter. It requires a +`HarnessV1SandboxProvider` and does not implicitly reuse local Pi OAuth; use +`pi:local` when the goal is to reuse the user's existing Pi setup. `registerBuiltinAgentHarnesses()` also registers the [ACP](#acp) harnesses (`acp`, `acp:gemini`, `acp:claude-code`). diff --git a/packages/core/docs/content/locales/ar-SA/harness-agents.mdx b/packages/core/docs/content/locales/ar-SA/harness-agents.mdx index cdfd1710b1..49b68e6135 100644 --- a/packages/core/docs/content/locales/ar-SA/harness-agents.mdx +++ b/packages/core/docs/content/locales/ar-SA/harness-agents.mdx @@ -94,14 +94,14 @@ UI مع [`AgentChatRuntime`](/docs/native-chat-ui#byo-agent-runtimes)؛ دع | ---------------------------- | ----------- | ----------- | --------- | | `ai-sdk-harness:claude-code` | رمز Claude | نعم | نعم | | `ai-sdk-harness:codex` | Codex | نعم | لا | -| `ai-sdk-harness:pi` | باي | لا | نعم | - -حزم وقت التشغيل الخاصة بها هي **تبعيات نظير اختيارية** ويتم تحميلها ببطء، لذا -التطبيق الذي لا يستخدم أداة التثبيت مطلقًا لا يدفع ثمنه. يحمل كل محول -تلميح `installPackage` (على سبيل المثال `@ai-sdk/harness@canary -@ai-sdk/harness-codex@canary`); `resolveAgentHarness` يُجري تثبيتًا واضحًا -خطأ إذا كانت الحزم مفقودة، و`isAgentHarnessPackageInstalled(entry)` -يتيح لك التحقق أولاً. +| `ai-sdk-harness:pi` | Pi | نعم | نعم | +| `pi:local` | Pi محلي | لا | لا | + +حزم التشغيل هي **تبعيات نظير اختيارية** تُحمّل عند الطلب. يعرض `resolveAgentHarness` تلميح التثبيت عند غياب الحزمة. + +### Pi المحلي في دردشة التطبيق {#local-pi-app-chat} + +يشغّل `pi:local` الحزمة `@earendil-works/pi-coding-agent` داخل عملية Node المحلية ويستخدم افتراضيًا دليل Pi المعتاد `~/.pi/agent`. يبقى اختيار النموذج وOAuth تحت إدارة Pi؛ ولا يقرأ Agent-Native ملف المصادقة أو ينسخه أو يخزنه، بينما تحفظ SQL مرجع الجلسة المعتم فقط. فعّله فقط مع حارس صريح مغلق افتراضيًا يتحقق من وضع التطوير وloopback. يعمل في المقدمة فقط، ويرفض المرفقات حاليًا، وتفشل إجراءات `needsApproval` بأمان. أما `ai-sdk-harness:pi` فهو محول sandbox ولا يعيد استخدام تسجيل دخول Pi المحلي تلقائيًا. يسجل `registerBuiltinAgentHarnesses()` أيضًا أحزمة [ACP](#acp) (`acp`, `acp:gemini`, `acp:claude-code`). diff --git a/packages/core/docs/content/locales/de-DE/harness-agents.mdx b/packages/core/docs/content/locales/de-DE/harness-agents.mdx index 996a8776ee..ea1c3527ce 100644 --- a/packages/core/docs/content/locales/de-DE/harness-agents.mdx +++ b/packages/core/docs/content/locales/de-DE/harness-agents.mdx @@ -94,14 +94,14 @@ Spawn-Hintergrund/Subagent läuft mit [Custom Agents & Teams](/docs/agent-teams) | ---------------------------- | ----------- | ------- | ------------- | | `ai-sdk-harness:claude-code` | Claude-Code | Ja | Ja | | `ai-sdk-harness:codex` | Codex | Ja | nein | -| `ai-sdk-harness:pi` | Pi | nein | Ja | - -Ihre Laufzeitpakete sind **optionale Peer-Abhängigkeiten** und werden träge geladen, also ein -Apps, die niemals ein Geschirr verwenden, zahlen nicht dafür. Jeder Adapter trägt eine -`installPackage`-Hinweis (zum Beispiel „@ai-sdk/harness@canary“ -@ai-sdk/harness-codex@canary`); `resolveAgentHarness`löst eine klare Installation aus -Fehler, wenn die Pakete fehlen, und`isAgentHarnessPackageInstalled(entry)` -ermöglicht es Ihnen, zuerst zu prüfen. +| `ai-sdk-harness:pi` | Pi | Ja | Ja | +| `pi:local` | Lokales Pi | nein | nein | + +Ihre Laufzeitpakete sind **optionale Peer-Abhängigkeiten** und werden verzögert geladen. `resolveAgentHarness` meldet fehlende Pakete mit einem Installationshinweis. + +### Lokales Pi im App-Chat {#local-pi-app-chat} + +`pi:local` führt `@earendil-works/pi-coding-agent` im lokalen Node-Prozess aus und verwendet standardmäßig das normale Pi-Agentenverzeichnis `~/.pi/agent`. Pi verwaltet Modellwahl und OAuth; Agent-Native liest, kopiert oder speichert die Auth-Datei nicht und speichert in SQL nur den undurchsichtigen Sitzungsverweis. Aktivieren Sie dies nur mit einem expliziten, fehlersicheren Guard für Entwicklung und Loopback. Der Lauf ist nur im Vordergrund, Anhänge werden noch abgelehnt und Aktionen mit `needsApproval` schlagen sicher fehl. `ai-sdk-harness:pi` bleibt der sandboxbasierte Adapter und übernimmt lokale Pi-Anmeldung nicht automatisch. `registerBuiltinAgentHarnesses()` registriert auch die [ACP](#acp)-Kabelbäume (`acp`, `acp:gemini`, `acp:claude-code`). diff --git a/packages/core/docs/content/locales/es-ES/harness-agents.mdx b/packages/core/docs/content/locales/es-ES/harness-agents.mdx index e776289428..06973d79c4 100644 --- a/packages/core/docs/content/locales/es-ES/harness-agents.mdx +++ b/packages/core/docs/content/locales/es-ES/harness-agents.mdx @@ -94,14 +94,14 @@ El fondo generado/el subagente se ejecuta con [Custom Agents & Teams](/docs/agen | ---------------------------- | ------------------- | ------------- | ------------ | | `ai-sdk-harness:claude-code` | Código Claude | sí | sí | | `ai-sdk-harness:codex` | Codex | sí | no | -| `ai-sdk-harness:pi` | Pi | no | sí | - -Sus paquetes de tiempo de ejecución son **dependencias de pares opcionales** y se cargan con pereza, por lo que -la aplicación que nunca usa un arnés no paga por ello. Cada adaptador lleva un -Pista `installPackage` (por ejemplo `@ai-sdk/harness@canary -@ai-sdk/harness-codex@canary`); `resolveAgentHarness` genera una instalación clara -error si faltan los paquetes y `isAgentHarnessPackageInstalled(entry)` -te permite comprobarlo primero. +| `ai-sdk-harness:pi` | Pi | sí | sí | +| `pi:local` | Pi local | no | no | + +Sus paquetes de ejecución son **dependencias opcionales** y se cargan de forma diferida. `resolveAgentHarness` devuelve una indicación de instalación cuando falta un paquete. + +### Pi local en el chat de la aplicación {#local-pi-app-chat} + +`pi:local` ejecuta `@earendil-works/pi-coding-agent` en el proceso Node local y usa de forma predeterminada el directorio normal de Pi, `~/.pi/agent`. Pi conserva la selección de modelo y OAuth; Agent-Native nunca lee, copia ni almacena el archivo de autenticación y SQL solo guarda la referencia opaca de sesión. Actívalo únicamente con una protección explícita y cerrada por defecto para desarrollo y loopback. Solo funciona en primer plano, todavía rechaza adjuntos y las acciones con `needsApproval` fallan de forma segura. `ai-sdk-harness:pi` sigue siendo el adaptador con sandbox y no reutiliza automáticamente la sesión local de Pi. `registerBuiltinAgentHarnesses()` también registra los arneses [ACP](#acp) (`acp`, `acp:gemini`, `acp:claude-code`). diff --git a/packages/core/docs/content/locales/fr-FR/harness-agents.mdx b/packages/core/docs/content/locales/fr-FR/harness-agents.mdx index b7b05bd3e4..14a85704d8 100644 --- a/packages/core/docs/content/locales/fr-FR/harness-agents.mdx +++ b/packages/core/docs/content/locales/fr-FR/harness-agents.mdx @@ -94,14 +94,14 @@ apparaître en arrière-plan/sous-agent avec [Custom Agents & Teams](/docs/agent | ---------------------------- | ----------- | ----------- | ------------ | | `ai-sdk-harness:claude-code` | Code Claude | oui | oui | | `ai-sdk-harness:codex` | Codex | oui | non | -| `ai-sdk-harness:pi` | Pi | non | oui | - -Leurs packages d'exécution sont des **dépendances homologues facultatives** et se chargent paresseusement, donc -une application qui n'utilise jamais de harnais ne paie pas pour cela. Chaque adaptateur porte un -Indice `installPackage` (par exemple `@ai-sdk/harness@canary -@ai-sdk/harness-codex@canary`); `resolveAgentHarness` lance une installation claire -erreur si les packages sont manquants, et `isAgentHarnessPackageInstalled(entry)` -vous permet de vérifier en premier. +| `ai-sdk-harness:pi` | Pi | oui | oui | +| `pi:local` | Pi local | non | non | + +Leurs paquets d’exécution sont des **dépendances homologues facultatives** chargées à la demande. `resolveAgentHarness` fournit une indication d’installation lorsqu’un paquet manque. + +### Pi local dans le chat de l’application {#local-pi-app-chat} + +`pi:local` exécute `@earendil-works/pi-coding-agent` dans le processus Node local et utilise par défaut le répertoire Pi normal, `~/.pi/agent`. Pi reste propriétaire du choix du modèle et d’OAuth ; Agent-Native ne lit, ne copie et ne stocke jamais le fichier d’authentification, et SQL ne conserve que la référence de session opaque. Activez-le uniquement avec une garde explicite et fermée par défaut pour le développement et le loopback. Il fonctionne seulement au premier plan, refuse encore les pièces jointes et les actions `needsApproval` échouent de façon sûre. `ai-sdk-harness:pi` reste l’adaptateur sandboxé et ne réutilise pas automatiquement la connexion Pi locale. `registerBuiltinAgentHarnesses()` enregistre également les harnais [ACP](#acp) (`acp`, `acp:gemini`, `acp:claude-code`). diff --git a/packages/core/docs/content/locales/hi-IN/harness-agents.mdx b/packages/core/docs/content/locales/hi-IN/harness-agents.mdx index 7dd52532e7..41b441f429 100644 --- a/packages/core/docs/content/locales/hi-IN/harness-agents.mdx +++ b/packages/core/docs/content/locales/hi-IN/harness-agents.mdx @@ -94,14 +94,14 @@ search: "हार्नेस एजेंट एजेंटहार्ने | ---------------------------- | ---------- | --------- | ------- | | `ai-sdk-harness:claude-code` | Claude कोड | हां | हां | | `ai-sdk-harness:codex` | Codex | हाँ | नहीं | -| `ai-sdk-harness:pi` | पी | नहीं | हाँ | - -उनके रनटाइम पैकेज **वैकल्पिक सहकर्मी निर्भरता** हैं और आलस्य से लोड होते हैं, इसलिए एक -ऐप जो कभी भी हार्नेस का उपयोग नहीं करता वह इसके लिए भुगतान नहीं करता है। प्रत्येक एडॉप्टर में एक -`installPackage` संकेत (उदाहरण के लिए `@ai-sdk/harness@canary -@ai-sdk/harness-codex@canary`); `resolveAgentHarness` एक स्पष्ट इंस्टॉल फेंकता है -यदि पैकेज गायब हैं तो त्रुटि, और `isAgentHarnessPackageInstalled(entry)` -आपको पहले जांच करने देता है। +| `ai-sdk-harness:pi` | Pi | हाँ | हाँ | +| `pi:local` | स्थानीय Pi | नहीं | नहीं | + +रनटाइम पैकेज मांग पर लोड होने वाली **वैकल्पिक पीयर निर्भरताएँ** हैं। पैकेज न मिलने पर `resolveAgentHarness` इंस्टॉल संकेत देता है। + +### ऐप चैट में स्थानीय Pi {#local-pi-app-chat} + +`pi:local` स्थानीय Node प्रक्रिया में `@earendil-works/pi-coding-agent` चलाता है और डिफ़ॉल्ट रूप से Pi की सामान्य डायरेक्टरी `~/.pi/agent` का उपयोग करता है। मॉडल चयन और OAuth Pi के नियंत्रण में रहते हैं; Agent-Native auth फ़ाइल को पढ़ता, कॉपी या संग्रहीत नहीं करता और SQL में केवल अपारदर्शी सत्र संदर्भ रहता है। इसे केवल विकास और loopback जाँचने वाले स्पष्ट fail-closed guard के साथ सक्षम करें। यह केवल foreground में चलता है, अभी attachments अस्वीकार करता है और `needsApproval` actions सुरक्षित रूप से विफल होते हैं। `ai-sdk-harness:pi` sandbox adapter है और स्थानीय Pi login को स्वतः पुनः उपयोग नहीं करता। `registerBuiltinAgentHarnesses()` [ACP](#acp) हार्नेस को भी पंजीकृत करता है (`acp`, `acp:gemini`, `acp:claude-code`). diff --git a/packages/core/docs/content/locales/ja-JP/harness-agents.mdx b/packages/core/docs/content/locales/ja-JP/harness-agents.mdx index e6ba22860c..7933ca5e92 100644 --- a/packages/core/docs/content/locales/ja-JP/harness-agents.mdx +++ b/packages/core/docs/content/locales/ja-JP/harness-agents.mdx @@ -94,14 +94,14 @@ UI と [`AgentChatRuntime`](/docs/native-chat-ui#byo-agent-runtimes);させて | ---------------------------- | ------------- | -------------- | ------ | | `ai-sdk-harness:claude-code` | Claude コード | はい | はい | | `ai-sdk-harness:codex` | Codex | はい | いいえ | -| `ai-sdk-harness:pi` | 円周率 | いいえ | はい | - -それらのランタイム パッケージは **オプションのピア依存関係**であり、遅延ロードされるため、 -ハーネスを使用しないアプリには料金がかかりません。各アダプターには、 -`installPackage` ヒント (例: `@ai-sdk/harness@canary -@ai-sdk/harness-codex@canary`); `resolveAgentHarness` はクリア インストールをスローします -パッケージが見つからない場合のエラー、および `isAgentHarnessPackageInstalled(entry)` -最初に確認できます。 +| `ai-sdk-harness:pi` | Pi | はい | はい | +| `pi:local` | ローカル Pi | いいえ | いいえ | + +ランタイム パッケージは遅延ロードされる**オプションのピア依存関係**です。パッケージがない場合、`resolveAgentHarness` はインストール方法を示します。 + +### アプリチャットのローカル Pi {#local-pi-app-chat} + +`pi:local` はローカル Node プロセスで `@earendil-works/pi-coding-agent` を実行し、既定で通常の Pi ディレクトリ `~/.pi/agent` を使用します。モデル選択と OAuth は Pi が所有し、Agent-Native は認証ファイルを読み取り、コピー、保存しません。SQL には不透明なセッション参照だけを保存します。開発環境と loopback を検証する、明示的でフェイルクローズなガードがある場合だけ有効にしてください。フォアグラウンド専用で、添付ファイルはまだ拒否され、`needsApproval` アクションは安全に失敗します。`ai-sdk-harness:pi` は sandbox 用で、ローカル Pi ログインを自動再利用しません。 `registerBuiltinAgentHarnesses()` は [ACP](#acp) ハーネスも登録します (`acp`, `acp:gemini`, `acp:claude-code`). diff --git a/packages/core/docs/content/locales/ko-KR/harness-agents.mdx b/packages/core/docs/content/locales/ko-KR/harness-agents.mdx index f294b62ce9..5deb0b03cd 100644 --- a/packages/core/docs/content/locales/ko-KR/harness-agents.mdx +++ b/packages/core/docs/content/locales/ko-KR/harness-agents.mdx @@ -94,14 +94,14 @@ UI 및 [`AgentChatRuntime`](/docs/native-chat-ui#byo-agent-runtimes); 하자 | ---------------------------- | ----------- | ---------- | ---------- | | `ai-sdk-harness:claude-code` | Claude 코드 | 그렇습니다 | 그렇습니다 | | `ai-sdk-harness:codex` | Codex | 그렇습니다 | 아니요 | -| `ai-sdk-harness:pi` | 파이 | 아니요 | 그렇습니다 | - -런타임 패키지는 **선택적 피어 종속성**이며 느리게 로드되므로 -하네스를 전혀 사용하지 않는 앱은 비용을 지불하지 않습니다. 각 어댑터에는 -`installPackage` 힌트(예: `@ai-sdk/harness@canary -@ai-sdk/harness-codex@canary`); `resolveAgentHarness`는 명확한 설치를 발생시킵니다. -패키지가 누락된 경우 오류가 발생하고 `isAgentHarnessPackageInstalled(entry)` -먼저 확인해 보세요. +| `ai-sdk-harness:pi` | Pi | 예 | 예 | +| `pi:local` | 로컬 Pi | 아니요 | 아니요 | + +런타임 패키지는 요청 시 로드되는 **선택적 피어 종속성**입니다. 패키지가 없으면 `resolveAgentHarness`가 설치 힌트를 제공합니다. + +### 앱 채팅의 로컬 Pi {#local-pi-app-chat} + +`pi:local`은 로컬 Node 프로세스에서 `@earendil-works/pi-coding-agent`를 실행하고 기본적으로 Pi의 일반 디렉터리 `~/.pi/agent`를 사용합니다. 모델 선택과 OAuth는 Pi가 소유하며 Agent-Native는 인증 파일을 읽거나 복사하거나 저장하지 않습니다. SQL에는 불투명한 세션 참조만 저장됩니다. 개발 모드와 loopback을 확인하는 명시적인 fail-closed 가드가 있을 때만 활성화하세요. foreground 전용이며 아직 첨부 파일을 거부하고 `needsApproval` 작업은 안전하게 실패합니다. `ai-sdk-harness:pi`는 sandbox 어댑터이며 로컬 Pi 로그인을 자동으로 재사용하지 않습니다. `registerBuiltinAgentHarnesses()`는 [ACP](#acp) 하니스도 등록합니다 (`acp`, `acp:gemini`, `acp:claude-code`). diff --git a/packages/core/docs/content/locales/pt-BR/harness-agents.mdx b/packages/core/docs/content/locales/pt-BR/harness-agents.mdx index 52c52dc353..c90017a27f 100644 --- a/packages/core/docs/content/locales/pt-BR/harness-agents.mdx +++ b/packages/core/docs/content/locales/pt-BR/harness-agents.mdx @@ -94,14 +94,14 @@ gerar execução de segundo plano/subagente com [Custom Agents & Teams](/docs/ag | ---------------------------- | ----------------- | -------------- | ---------- | | `ai-sdk-harness:claude-code` | Código Claude | sim | sim | | `ai-sdk-harness:codex` | Codex | sim | não | -| `ai-sdk-harness:pi` | Pi | não | sim | - -Seus pacotes de tempo de execução são **dependências de pares opcionais** e carregam lentamente, portanto, -aplicativo que nunca usa arnês não paga por isso. Cada adaptador carrega um -Dica `installPackage` (por exemplo `@ai-sdk/harness@canary -@ai-sdk/harness-codex@canary`); `resolveAgentHarness` lança uma instalação clara -erro se os pacotes estiverem faltando e `isAgentHarnessPackageInstalled(entry)` -permite verificar primeiro. +| `ai-sdk-harness:pi` | Pi | sim | sim | +| `pi:local` | Pi local | não | não | + +Os pacotes de execução são **dependências opcionais** carregadas sob demanda. `resolveAgentHarness` fornece uma dica de instalação quando um pacote está ausente. + +### Pi local no chat do aplicativo {#local-pi-app-chat} + +`pi:local` executa `@earendil-works/pi-coding-agent` no processo Node local e usa por padrão o diretório normal do Pi, `~/.pi/agent`. O Pi continua responsável pela escolha do modelo e pelo OAuth; o Agent-Native nunca lê, copia ou armazena o arquivo de autenticação, e o SQL guarda apenas a referência opaca da sessão. Ative somente com uma proteção explícita que negue por padrão fora de desenvolvimento e loopback. Ele funciona apenas em primeiro plano, ainda rejeita anexos e ações com `needsApproval` falham com segurança. `ai-sdk-harness:pi` continua sendo o adaptador em sandbox e não reutiliza automaticamente o login local do Pi. `registerBuiltinAgentHarnesses()` também registra os chicotes [ACP](#acp) (`acp`, `acp:gemini`, `acp:claude-code`). diff --git a/packages/core/docs/content/locales/zh-CN/harness-agents.mdx b/packages/core/docs/content/locales/zh-CN/harness-agents.mdx index 3a748c0443..90f04ee201 100644 --- a/packages/core/docs/content/locales/zh-CN/harness-agents.mdx +++ b/packages/core/docs/content/locales/zh-CN/harness-agents.mdx @@ -94,14 +94,14 @@ UI 与 [`AgentChatRuntime`](/docs/native-chat-ui#byo-agent-runtimes);让一个 | ---------------------------- | ---------- | ---- | ---- | | `ai-sdk-harness:claude-code` | Claude代码 | 是的 | 是的 | | `ai-sdk-harness:codex` | Codex | 是的 | 没有 | -| `ai-sdk-harness:pi` | 圆周率 | 没有 | 是的 | - -它们的运行时包是**可选的对等依赖项**并且延迟加载,因此 -从不使用安全带的应用程序无需付费。每个适配器都带有一个 -`installPackage` 提示(例如`@ai-sdk/harness@canary -@ai-sdk/harness-codex@canary`); `resolveAgentHarness`抛出一个明确的安装 -如果缺少包,则会出现错误,并且 `isAgentHarnessPackageInstalled(entry)` -让您先检查。 +| `ai-sdk-harness:pi` | Pi | 是 | 是 | +| `pi:local` | 本地 Pi | 否 | 否 | + +运行时包是按需加载的**可选对等依赖项**。缺少包时,`resolveAgentHarness` 会给出安装提示。 + +### 应用聊天中的本地 Pi {#local-pi-app-chat} + +`pi:local` 在本地 Node 进程中运行 `@earendil-works/pi-coding-agent`,默认使用 Pi 的常规目录 `~/.pi/agent`。模型选择和 OAuth 仍由 Pi 管理;Agent-Native 不会读取、复制或存储认证文件,SQL 只保存不透明的会话引用。仅在显式、默认拒绝且验证开发模式和 loopback 的守卫下启用。它仅支持 foreground,目前拒绝附件,带 `needsApproval` 的操作会安全失败。`ai-sdk-harness:pi` 仍是 sandbox 适配器,不会自动复用本地 Pi 登录。 `registerBuiltinAgentHarnesses()` 还注册了 [ACP](#acp) 线束 (`acp`, `acp:gemini`, `acp:claude-code`). diff --git a/packages/core/docs/content/locales/zh-TW/harness-agents.mdx b/packages/core/docs/content/locales/zh-TW/harness-agents.mdx index 66b8601195..c1997f53b4 100644 --- a/packages/core/docs/content/locales/zh-TW/harness-agents.mdx +++ b/packages/core/docs/content/locales/zh-TW/harness-agents.mdx @@ -94,14 +94,14 @@ UI 與 [`AgentChatRuntime`](/docs/native-chat-ui#byo-agent-runtimes);讓一個 | ---------------------------- | ------------- | ---- | ---- | | `ai-sdk-harness:claude-code` | Claude 程式碼 | 是的 | 是的 | | `ai-sdk-harness:codex` | Codex | 是的 | 沒有 | -| `ai-sdk-harness:pi` | 圓週率 | 沒有 | 是的 | - -它們的執行時包是**可選的對等依賴項**並且延遲載入,因此 -從不使用安全帶的應用程式無需付費。每個轉接器都帶有一個 -`installPackage` 提示(例如`@ai-sdk/harness@canary -@ai-sdk/harness-codex@canary`); `resolveAgentHarness`拋出一個明確的安裝 -如果缺少包,則會出現錯誤,並且 `isAgentHarnessPackageInstalled(entry)` -讓您先檢查。 +| `ai-sdk-harness:pi` | Pi | 是 | 是 | +| `pi:local` | 本機 Pi | 否 | 否 | + +執行時套件是按需載入的**可選對等依賴項**。缺少套件時,`resolveAgentHarness` 會提供安裝提示。 + +### 應用程式聊天中的本機 Pi {#local-pi-app-chat} + +`pi:local` 在本機 Node 程序中執行 `@earendil-works/pi-coding-agent`,預設使用 Pi 的一般目錄 `~/.pi/agent`。模型選擇與 OAuth 仍由 Pi 管理;Agent-Native 不會讀取、複製或儲存認證檔案,SQL 只保存不透明的工作階段參照。僅在明確、預設拒絕且驗證開發模式與 loopback 的守衛下啟用。它僅支援 foreground,目前拒絕附件,含 `needsApproval` 的操作會安全失敗。`ai-sdk-harness:pi` 仍是 sandbox 轉接器,不會自動重用本機 Pi 登入。 `registerBuiltinAgentHarnesses()` 還註冊了 [ACP](#acp) 線束 (`acp`, `acp:gemini`, `acp:claude-code`). diff --git a/packages/core/src/agent/harness/ai-sdk-adapter.spec.ts b/packages/core/src/agent/harness/ai-sdk-adapter.spec.ts index fd7e1614fe..81b88089f1 100644 --- a/packages/core/src/agent/harness/ai-sdk-adapter.spec.ts +++ b/packages/core/src/agent/harness/ai-sdk-adapter.spec.ts @@ -8,6 +8,7 @@ import { aiSdkHarnessPartToEvents, createCodexCliAuthSandboxHook, normalizeCodexCliAuthConfig, + toAiSdkHarnessTools, } from "./ai-sdk-adapter.js"; const tempDirs: string[] = []; @@ -103,6 +104,57 @@ describe("aiSdkHarnessPartToEvents", () => { }); }); +describe("toAiSdkHarnessTools", () => { + it("grants a host tool only after the AI SDK approval gate ran for the same call", async () => { + const execute = vi.fn(async () => "ok"); + const converted = toAiSdkHarnessTools( + { + publish: { + description: "Publish", + inputSchema: { type: "object" }, + needsApproval: async () => true, + execute, + }, + }, + { + tool: (definition) => definition, + jsonSchema: (schema) => schema, + }, + ); + const publish = converted.publish as { + needsApproval: ( + input: unknown, + context: { toolCallId: string }, + ) => Promise; + execute: ( + input: unknown, + context: { toolCallId: string }, + ) => Promise; + }; + + await publish.execute({}, { toolCallId: "call-1" }); + expect(execute).toHaveBeenLastCalledWith( + {}, + expect.objectContaining({ toolCallId: "call-1", approved: false }), + ); + + await expect( + publish.needsApproval({}, { toolCallId: "call-2" }), + ).resolves.toBe(true); + await publish.execute({}, { toolCallId: "call-2" }); + expect(execute).toHaveBeenLastCalledWith( + {}, + expect.objectContaining({ toolCallId: "call-2", approved: true }), + ); + + await publish.execute({}, { toolCallId: "call-2" }); + expect(execute).toHaveBeenLastCalledWith( + {}, + expect.objectContaining({ toolCallId: "call-2", approved: false }), + ); + }); +}); + describe("codexCliAuth sandbox hook", () => { it("copies local Codex CLI auth into the sandbox before an existing hook runs", async () => { const authDir = makeTempDir(); diff --git a/packages/core/src/agent/harness/ai-sdk-adapter.ts b/packages/core/src/agent/harness/ai-sdk-adapter.ts index 73c9a1aac8..d9363064c8 100644 --- a/packages/core/src/agent/harness/ai-sdk-adapter.ts +++ b/packages/core/src/agent/harness/ai-sdk-adapter.ts @@ -8,6 +8,7 @@ import type { AgentHarnessContinueInput, AgentHarnessCreateSessionOptions, AgentHarnessEvent, + AgentHarnessHostTool, AgentHarnessSession, AgentHarnessTurnInput, } from "./types.js"; @@ -93,7 +94,7 @@ const RUNTIME_IMPORTS: Record< packageName: "@ai-sdk/harness-pi", exportNames: ["pi", "createPi"], label: "Pi", - sandbox: false, + sandbox: true, }, }; @@ -124,9 +125,10 @@ export function createAiSdkHarnessAdapter( installPackage: `@ai-sdk/harness@canary ${runtime.packageName}@canary`, capabilities, async createSession(sessionOptions) { - const [{ HarnessAgent }, runtimeModule] = await Promise.all([ + const [{ HarnessAgent }, runtimeModule, aiModule] = await Promise.all([ dynamicImport("@ai-sdk/harness/agent"), dynamicImport(runtime.packageName), + sessionOptions.tools ? dynamicImport("ai") : Promise.resolve(null), ]); const exportName = runtime.exportNames.find( (name) => runtimeModule[name], @@ -146,6 +148,10 @@ export function createAiSdkHarnessAdapter( ? harnessFactory(options.harnessOptions) : harnessFactory; const agentOptions = agentOptionsWithCodexCliAuth(options); + const tools = + sessionOptions.tools && aiModule + ? toAiSdkHarnessTools(sessionOptions.tools, aiModule) + : undefined; const agent = new HarnessAgent({ ...agentOptions, harness, @@ -154,7 +160,7 @@ export function createAiSdkHarnessAdapter( ? { instructions: sessionOptions.instructions } : {}), ...(sessionOptions.skills ? { skills: sessionOptions.skills } : {}), - ...(sessionOptions.tools ? { tools: sessionOptions.tools } : {}), + ...(tools ? { tools } : {}), permissionMode: sessionOptions.permissionMode ?? options.permissionMode ?? @@ -167,6 +173,55 @@ export function createAiSdkHarnessAdapter( }; } +export function toAiSdkHarnessTools( + tools: Record, + ai: { + tool: (definition: Record) => unknown; + jsonSchema: (schema: Record) => unknown; + }, +): Record { + return Object.fromEntries( + Object.entries(tools).map(([name, hostTool]) => { + const approvalRequiredCallIds = new Set(); + return [ + name, + ai.tool({ + description: hostTool.description, + inputSchema: ai.jsonSchema(hostTool.inputSchema), + ...(hostTool.needsApproval + ? { + needsApproval: async ( + input: unknown, + context: { toolCallId?: string } = {}, + ) => { + const required = await hostTool.needsApproval!(input); + if (context.toolCallId) { + if (required) + approvalRequiredCallIds.add(context.toolCallId); + else approvalRequiredCallIds.delete(context.toolCallId); + } + return required; + }, + } + : {}), + execute: ( + input: unknown, + context: { toolCallId?: string; abortSignal?: AbortSignal } = {}, + ) => { + const toolCallId = context.toolCallId ?? `${name}-call`; + const approved = approvalRequiredCallIds.delete(toolCallId); + return hostTool.execute(input, { + toolCallId, + abortSignal: context.abortSignal, + approved, + }); + }, + }), + ]; + }), + ); +} + function agentOptionsWithCodexCliAuth( options: AiSdkHarnessAdapterOptions, ): Record { diff --git a/packages/core/src/agent/harness/builtin.ts b/packages/core/src/agent/harness/builtin.ts index 78d0adba95..4b01656287 100644 --- a/packages/core/src/agent/harness/builtin.ts +++ b/packages/core/src/agent/harness/builtin.ts @@ -7,6 +7,10 @@ import { createAiSdkHarnessAdapter, type AiSdkHarnessRuntime, } from "./ai-sdk-adapter.js"; +import { + createLocalPiHarnessAdapter, + type LocalPiHarnessAdapterOptions, +} from "./local-pi-adapter.js"; import { registerAgentHarness } from "./registry.js"; const AI_SDK_HARNESS_RUNTIMES: AiSdkHarnessRuntime[] = [ @@ -32,6 +36,19 @@ export function registerBuiltinAgentHarnesses(): void { }); } + const localPiAdapter = createLocalPiHarnessAdapter(); + registerAgentHarness({ + name: localPiAdapter.name, + label: localPiAdapter.label, + description: localPiAdapter.description, + installPackage: localPiAdapter.installPackage, + capabilities: localPiAdapter.capabilities, + create: (config) => + createLocalPiHarnessAdapter( + (config ?? {}) as LocalPiHarnessAdapterOptions, + ), + }); + // Generic ACP entry: resolve with { command, args } for any ACP agent. const acpAdapter = createAcpHarnessAdapter({ command: "acp" }); registerAgentHarness({ diff --git a/packages/core/src/agent/harness/chat-tools.spec.ts b/packages/core/src/agent/harness/chat-tools.spec.ts new file mode 100644 index 0000000000..b820dd92a3 --- /dev/null +++ b/packages/core/src/agent/harness/chat-tools.spec.ts @@ -0,0 +1,97 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + execute: vi.fn(), + cacheKey: vi.fn(() => "approval-key"), +})); + +vi.mock("../production-agent.js", () => ({ + executeAgentToolCall: mocks.execute, + toolCallCacheKey: mocks.cacheKey, +})); + +const { createAgentHarnessActionTools } = await import("./chat-tools.js"); + +describe("createAgentHarnessActionTools", () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.execute.mockResolvedValue({ status: "completed", output: "ok" }); + }); + + it("excludes actions that are unavailable to agent tools", () => { + const tools = createAgentHarnessActionTools({ + actions: { + visible: action(), + hidden: { ...action(), agentTool: false }, + networkOnly: { ...action(), toolCallable: false }, + }, + ownerEmail: "owner@example.com", + }); + + expect(Object.keys(tools)).toEqual(["visible"]); + }); + + it("executes through the guarded Agent Native tool runtime", async () => { + const tools = createAgentHarnessActionTools({ + actions: { lookup: action() }, + ownerEmail: "owner@example.com", + orgId: "org-1", + threadId: "thread-1", + turnId: "turn-1", + }); + + await expect( + tools.lookup!.execute( + { query: "hello" }, + { toolCallId: "call-1", abortSignal: new AbortController().signal }, + ), + ).resolves.toBe("ok"); + expect(mocks.execute).toHaveBeenCalledWith( + expect.objectContaining({ + name: "lookup", + input: { query: "hello" }, + callId: "call-1", + ownerEmail: "owner@example.com", + orgId: "org-1", + caller: "tool", + threadId: "thread-1", + turnId: "turn-1", + }), + ); + }); + + it("fails closed before an adapter supplies a human-approval grant", async () => { + const guarded = action(); + guarded.needsApproval = async () => { + throw new Error("predicate failure"); + }; + const tools = createAgentHarnessActionTools({ + actions: { publish: guarded }, + ownerEmail: "owner@example.com", + }); + + await expect(tools.publish!.needsApproval!({})).resolves.toBe(true); + await expect( + tools.publish!.execute({}, { toolCallId: "call-1" }), + ).rejects.toThrow("requires human approval"); + expect(mocks.execute).not.toHaveBeenCalled(); + + await tools.publish!.execute({}, { toolCallId: "call-1", approved: true }); + expect(mocks.execute).toHaveBeenCalledWith( + expect.objectContaining({ approvedToolCalls: ["approval-key"] }), + ); + }); +}); + +function action() { + return { + tool: { + description: "Test action", + parameters: { + type: "object" as const, + properties: { query: { type: "string" } }, + }, + }, + run: vi.fn(async () => "ok"), + }; +} diff --git a/packages/core/src/agent/harness/chat-tools.ts b/packages/core/src/agent/harness/chat-tools.ts new file mode 100644 index 0000000000..7ab44d9fa8 --- /dev/null +++ b/packages/core/src/agent/harness/chat-tools.ts @@ -0,0 +1,104 @@ +import { + executeAgentToolCall, + toolCallCacheKey, + type ActionEntry, +} from "../production-agent.js"; +import type { AgentHarnessHostTool } from "./types.js"; + +export interface CreateAgentHarnessActionToolsOptions { + actions: Record; + ownerEmail: string; + orgId?: string | null; + threadId?: string; + turnId?: string; +} + +export function createAgentHarnessActionTools( + options: CreateAgentHarnessActionToolsOptions, +): Record { + return Object.fromEntries( + Object.entries(options.actions) + .filter( + ([, entry]) => + entry.agentTool !== false && entry.toolCallable !== false, + ) + .map(([name, entry]) => [ + name, + { + description: entry.tool.description, + inputSchema: entry.tool.parameters as unknown as Record< + string, + unknown + >, + readOnly: entry.readOnly === true, + ...(entry.needsApproval + ? { + needsApproval: (input: unknown) => + actionNeedsApproval(entry, input, options), + } + : {}), + execute: async ( + input: unknown, + context: { + toolCallId: string; + abortSignal?: AbortSignal; + approved?: boolean; + }, + ) => { + const needsApproval = await actionNeedsApproval( + entry, + input, + options, + ); + if (needsApproval && context.approved !== true) { + throw new Error( + `Agent Native action ${name} requires human approval.`, + ); + } + const result = await executeAgentToolCall({ + actions: options.actions, + name, + input, + callId: context.toolCallId, + signal: context.abortSignal, + ownerEmail: options.ownerEmail, + orgId: options.orgId, + caller: "tool", + threadId: options.threadId, + turnId: options.turnId, + ...(needsApproval && context.approved === true + ? { approvedToolCalls: [toolCallCacheKey(name, input)] } + : {}), + }); + if (result.status === "completed") return result.output; + if (result.status === "approval_required") { + throw new Error( + `Harness approval did not authorize Agent Native action ${name}.`, + ); + } + throw new Error(result.output); + }, + } satisfies AgentHarnessHostTool, + ]), + ); +} + +async function actionNeedsApproval( + entry: ActionEntry, + input: unknown, + options: CreateAgentHarnessActionToolsOptions, +): Promise { + if (entry.needsApproval === true) return true; + if (typeof entry.needsApproval !== "function") return false; + try { + return Boolean( + await entry.needsApproval(input, { + userEmail: options.ownerEmail, + orgId: options.orgId ?? null, + caller: "tool", + }), + ); + } catch { + return true; + } +} diff --git a/packages/core/src/agent/harness/index.ts b/packages/core/src/agent/harness/index.ts index 3ca0a316a7..75c39b189c 100644 --- a/packages/core/src/agent/harness/index.ts +++ b/packages/core/src/agent/harness/index.ts @@ -5,6 +5,8 @@ export type { AgentHarnessContinueInput, AgentHarnessCreateSessionOptions, AgentHarnessEvent, + AgentHarnessHostTool, + AgentHarnessHostToolContext, AgentHarnessMessage, AgentHarnessPermissionMode, AgentHarnessSession, @@ -31,13 +33,16 @@ export { markAgentHarnessSessionStopped, saveAgentHarnessSession, updateAgentHarnessSession, + type AgentHarnessSessionScope, type AgentHarnessSessionStatus, type SaveAgentHarnessSessionInput, type StoredAgentHarnessSession, } from "./store.js"; export { sendAgentHarnessEvent, + startAgentHarnessApprovalRun, startAgentHarnessRun, + type StartAgentHarnessApprovalRunOptions, type StartAgentHarnessRunOptions, } from "./runner.js"; export { @@ -51,13 +56,28 @@ export { type AgentHarnessLifecycleResult, type AgentHarnessOwnerScope, } from "./lifecycle.js"; +export { + createAgentHarnessActionTools, + type CreateAgentHarnessActionToolsOptions, +} from "./chat-tools.js"; export { aiSdkHarnessPartToEvents, createAiSdkHarnessAdapter, + toAiSdkHarnessTools, type AiSdkHarnessAdapterOptions, type AiSdkHarnessRuntime, type CodexCliAuthConfig, } from "./ai-sdk-adapter.js"; +export { + LOCAL_PI_PACKAGE, + createLocalPiHarnessAdapter, + filterLocalPiHostTools, + localPiHostToolsToDefinitions, + localPiSessionEventToHarnessEvents, + normalizeLocalPiResumeState, + type LocalPiHarnessAdapterOptions, + type LocalPiResumeState, +} from "./local-pi-adapter.js"; export { ACP_PACKAGE, acpAutoPermissionDecision, diff --git a/packages/core/src/agent/harness/local-pi-adapter.spec.ts b/packages/core/src/agent/harness/local-pi-adapter.spec.ts new file mode 100644 index 0000000000..05947535f6 --- /dev/null +++ b/packages/core/src/agent/harness/local-pi-adapter.spec.ts @@ -0,0 +1,106 @@ +import path from "node:path"; + +import { describe, expect, it, vi } from "vitest"; + +import { + filterLocalPiHostTools, + localPiHostToolsToDefinitions, + localPiSessionEventToHarnessEvents, + normalizeLocalPiResumeState, +} from "./local-pi-adapter.js"; +import type { AgentHarnessHostTool } from "./types.js"; + +describe("local Pi harness adapter", () => { + it("maps Pi streaming and tool events without exposing session data", () => { + expect( + localPiSessionEventToHarnessEvents({ + type: "message_update", + assistantMessageEvent: { type: "text_delta", delta: "hello" }, + }), + ).toEqual([{ type: "text-delta", text: "hello" }]); + expect( + localPiSessionEventToHarnessEvents({ + type: "tool_execution_start", + toolCallId: "call-1", + toolName: "read", + args: { path: "README.md" }, + }), + ).toEqual([ + { + type: "tool-start", + id: "call-1", + name: "read", + input: { path: "README.md" }, + }, + ]); + }); + + it("accepts resume files only from the configured Pi session directory", () => { + const sessions = path.resolve("/tmp/pi-agent/sessions"); + expect( + normalizeLocalPiResumeState( + { sessionFile: path.join(sessions, "project", "session.jsonl") }, + sessions, + ), + ).toEqual({ + sessionFile: path.join(sessions, "project", "session.jsonl"), + }); + expect( + normalizeLocalPiResumeState( + { sessionFile: "/tmp/pi-agent/auth.json" }, + sessions, + ), + ).toBeNull(); + expect( + normalizeLocalPiResumeState( + { sessionFile: "/tmp/other/session.jsonl" }, + sessions, + ), + ).toBeNull(); + }); + + it("keeps only explicitly read-only host tools in allow-reads mode", () => { + const read = hostTool(true); + const write = hostTool(false); + + expect( + Object.keys(filterLocalPiHostTools({ read, write }, "allow-reads")), + ).toEqual(["read"]); + expect( + Object.keys(filterLocalPiHostTools({ read, write }, "allow-edits")), + ).toEqual(["read", "write"]); + }); + + it("fails closed for actions that require approval", async () => { + const execute = vi.fn(async () => "published"); + const definitions = localPiHostToolsToDefinitions({ + publish: { + description: "Publish", + inputSchema: { type: "object" }, + needsApproval: async () => true, + execute, + }, + }); + const publish = definitions[0] as { + execute: ( + id: string, + input: unknown, + signal?: AbortSignal, + ) => Promise; + }; + + await expect(publish.execute("call-1", {})).rejects.toThrow( + "requires approval", + ); + expect(execute).not.toHaveBeenCalled(); + }); +}); + +function hostTool(readOnly: boolean): AgentHarnessHostTool { + return { + description: readOnly ? "Read" : "Write", + inputSchema: { type: "object" }, + readOnly, + execute: vi.fn(async () => "ok"), + }; +} diff --git a/packages/core/src/agent/harness/local-pi-adapter.ts b/packages/core/src/agent/harness/local-pi-adapter.ts new file mode 100644 index 0000000000..d60cddbf49 --- /dev/null +++ b/packages/core/src/agent/harness/local-pi-adapter.ts @@ -0,0 +1,367 @@ +import path from "node:path"; + +import type { + AgentHarnessAdapter, + AgentHarnessEvent, + AgentHarnessHostTool, + AgentHarnessPermissionMode, + AgentHarnessSession, + AgentHarnessTurnInput, +} from "./types.js"; + +export const LOCAL_PI_PACKAGE = "@earendil-works/pi-coding-agent@latest"; + +export interface LocalPiHarnessAdapterOptions { + label?: string; + description?: string; + cwd?: string; + agentDir?: string; + thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; + /** + * Pi-native tools available in app chat. Defaults to read-only filesystem + * tools; app operations are supplied separately as scoped host tools. + */ + nativeTools?: string[]; +} + +export interface LocalPiResumeState { + sessionFile: string; +} + +type PiModule = { + createAgentSession(options: Record): Promise<{ + session: PiSession; + }>; + DefaultResourceLoader: new (options: Record) => { + reload(): Promise; + }; + SessionManager: { + create(cwd: string, sessionDir?: string): unknown; + open(sessionFile: string, sessionDir?: string): unknown; + }; + SettingsManager: { + create(cwd?: string, agentDir?: string): unknown; + }; + getAgentDir(): string; +}; + +type PiSession = { + sessionId: string; + sessionFile?: string; + prompt(text: string, options?: Record): Promise; + subscribe(listener: (event: unknown) => void): () => void; + abort(): Promise; + dispose(): void; +}; + +const dynamicImport = new Function("specifier", "return import(specifier)") as ( + specifier: string, +) => Promise; + +export function createLocalPiHarnessAdapter( + options: LocalPiHarnessAdapterOptions = {}, +): AgentHarnessAdapter { + return { + name: "pi:local", + label: options.label ?? "Local Pi", + description: + options.description ?? + "Runs Pi in the local Node process using Pi-owned configuration and credentials.", + installPackage: LOCAL_PI_PACKAGE, + capabilities: { + sandbox: false, + resumable: true, + approvals: false, + hostTools: true, + fileEvents: false, + }, + async createSession(sessionOptions) { + const pi = await dynamicImport("@earendil-works/pi-coding-agent"); + const cwd = sessionOptions.cwd ?? options.cwd ?? process.cwd(); + const agentDir = options.agentDir ?? pi.getAgentDir(); + const sessionDir = path.join(agentDir, "sessions"); + const settingsManager = pi.SettingsManager.create(cwd, agentDir); + const resourceLoader = new pi.DefaultResourceLoader({ + cwd, + agentDir, + settingsManager, + ...(sessionOptions.instructions + ? { appendSystemPrompt: [sessionOptions.instructions] } + : {}), + }); + await resourceLoader.reload(); + + const resume = normalizeLocalPiResumeState( + sessionOptions.resumeState, + sessionDir, + ); + const sessionManager = resume + ? pi.SessionManager.open(resume.sessionFile, sessionDir) + : pi.SessionManager.create(cwd, sessionDir); + const permissionMode = sessionOptions.permissionMode ?? "allow-reads"; + const hostTools = filterLocalPiHostTools( + sessionOptions.tools ?? {}, + permissionMode, + ); + const customTools = localPiHostToolsToDefinitions(hostTools); + const nativeTools = uniqueToolNames([ + ...(options.nativeTools ?? ["read", "grep", "find", "ls"]), + ...Object.keys(hostTools), + ]); + const { session } = await pi.createAgentSession({ + cwd, + agentDir, + settingsManager, + resourceLoader, + sessionManager, + tools: nativeTools, + customTools, + ...(options.thinkingLevel + ? { thinkingLevel: options.thinkingLevel } + : {}), + }); + return new LocalPiHarnessSession( + sessionOptions.sessionId ?? session.sessionId, + session, + ); + }, + }; +} + +export function normalizeLocalPiResumeState( + value: unknown, + sessionDir: string, +): LocalPiResumeState | null { + if (!value || typeof value !== "object") return null; + const sessionFile = (value as { sessionFile?: unknown }).sessionFile; + if (typeof sessionFile !== "string" || !sessionFile.endsWith(".jsonl")) { + return null; + } + const resolvedDir = path.resolve(sessionDir); + const resolvedFile = path.resolve(sessionFile); + if ( + resolvedFile !== resolvedDir && + !resolvedFile.startsWith(`${resolvedDir}${path.sep}`) + ) { + return null; + } + return { sessionFile: resolvedFile }; +} + +export function localPiSessionEventToHarnessEvents( + value: unknown, +): AgentHarnessEvent[] { + if (!value || typeof value !== "object") return []; + const event = value as Record; + switch (event.type) { + case "message_update": { + const update = event.assistantMessageEvent; + if (update?.type === "text_delta" && typeof update.delta === "string") { + return [{ type: "text-delta", text: update.delta }]; + } + if ( + update?.type === "thinking_delta" && + typeof update.delta === "string" + ) { + return [{ type: "thinking-delta", text: update.delta }]; + } + return []; + } + case "tool_execution_start": + return [ + { + type: "tool-start", + id: stringOrUndefined(event.toolCallId), + name: stringOrFallback(event.toolName, "tool"), + input: event.args, + }, + ]; + case "tool_execution_end": + return [ + { + type: "tool-done", + id: stringOrUndefined(event.toolCallId), + name: stringOrFallback(event.toolName, "tool"), + result: event.result, + }, + ]; + case "compaction_start": + return [{ type: "activity", label: "Pi is compacting context" }]; + case "compaction_end": + return [{ type: "compaction" }]; + case "auto_retry_start": + return [{ type: "activity", label: "Pi is retrying the model request" }]; + case "extension_error": + return [ + { + type: "error", + error: stringOrFallback( + event.error?.message ?? event.error, + "Pi extension failed", + ), + }, + ]; + default: + return []; + } +} + +export function filterLocalPiHostTools( + tools: Record, + permissionMode: AgentHarnessPermissionMode, +): Record { + if (permissionMode !== "allow-reads") return tools; + return Object.fromEntries( + Object.entries(tools).filter(([, tool]) => tool.readOnly === true), + ); +} + +export function localPiHostToolsToDefinitions( + tools: Record, +): Array> { + return Object.entries(tools).map(([name, tool]) => ({ + name, + label: name, + description: tool.description, + parameters: tool.inputSchema, + async execute( + toolCallId: string, + params: unknown, + signal: AbortSignal | undefined, + ) { + if (await tool.needsApproval?.(params)) { + throw new Error( + `Agent Native action ${name} requires approval and is unavailable in the local Pi adapter.`, + ); + } + const result = await tool.execute(params, { + toolCallId, + abortSignal: signal, + approved: false, + }); + return { + content: [{ type: "text", text: stringifyLocalPiToolResult(result) }], + details: result, + }; + }, + })); +} + +class LocalPiHarnessSession implements AgentHarnessSession { + private disposed = false; + + constructor( + readonly id: string, + private readonly session: PiSession, + ) {} + + async *streamTurn( + input: AgentHarnessTurnInput, + ): AsyncIterable { + const prompt = input.prompt ?? messagesToPrompt(input.messages); + if (!prompt.trim()) throw new Error("Pi harness prompt is required."); + + const queue: AgentHarnessEvent[] = []; + let finished = false; + let failure: unknown; + let wake: (() => void) | undefined; + const notify = () => { + const current = wake; + wake = undefined; + current?.(); + }; + const unsubscribe = this.session.subscribe((event) => { + queue.push(...localPiSessionEventToHarnessEvents(event)); + notify(); + }); + const onAbort = () => { + void this.session.abort(); + }; + input.abortSignal?.addEventListener("abort", onAbort, { once: true }); + void this.session + .prompt(prompt, { source: "rpc" }) + .catch((error) => { + failure = error; + }) + .finally(() => { + finished = true; + notify(); + }); + + try { + while (!finished || queue.length > 0) { + const event = queue.shift(); + if (event) { + yield event; + continue; + } + await new Promise((resolve) => { + wake = resolve; + }); + } + if (failure) throw failure; + yield { type: "done" }; + } finally { + unsubscribe(); + input.abortSignal?.removeEventListener("abort", onAbort); + } + } + + async detach(): Promise { + const sessionFile = this.session.sessionFile; + if (!sessionFile) { + throw new Error("Pi did not create a persistent session file."); + } + this.dispose(); + return { sessionFile }; + } + + async stop(): Promise { + await this.session.abort(); + this.dispose(); + } + + async destroy(): Promise { + this.dispose(); + } + + private dispose(): void { + if (this.disposed) return; + this.disposed = true; + this.session.dispose(); + } +} + +function messagesToPrompt(messages: AgentHarnessTurnInput["messages"]): string { + return (messages ?? []) + .map((message) => { + const content = + typeof message.content === "string" + ? message.content + : stringifyLocalPiToolResult(message.content); + return `${message.role}: ${content}`; + }) + .join("\n\n"); +} + +function uniqueToolNames(names: string[]): string[] { + return Array.from(new Set(names.filter(Boolean))); +} + +function stringifyLocalPiToolResult(value: unknown): string { + if (typeof value === "string") return value; + if (value === undefined) return ""; + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +function stringOrUndefined(value: unknown): string | undefined { + return typeof value === "string" ? value : undefined; +} + +function stringOrFallback(value: unknown, fallback: string): string { + return typeof value === "string" && value ? value : fallback; +} diff --git a/packages/core/src/agent/harness/runner.spec.ts b/packages/core/src/agent/harness/runner.spec.ts index a0df1ebd70..a296c7db65 100644 --- a/packages/core/src/agent/harness/runner.spec.ts +++ b/packages/core/src/agent/harness/runner.spec.ts @@ -9,6 +9,9 @@ const mocks = vi.hoisted(() => ({ getAgentHarnessSession: vi.fn(), updateAgentHarnessSession: vi.fn(), markAgentHarnessSessionStopped: vi.fn(), + registerLiveAgentHarnessSession: vi.fn(), + releaseLiveAgentHarnessSession: vi.fn(), + resolveAgentHarnessApproval: vi.fn(), })); vi.mock("../run-manager.js", () => ({ @@ -22,7 +25,14 @@ vi.mock("./store.js", () => ({ markAgentHarnessSessionStopped: mocks.markAgentHarnessSessionStopped, })); -const { startAgentHarnessRun } = await import("./runner.js"); +vi.mock("./lifecycle.js", () => ({ + registerLiveAgentHarnessSession: mocks.registerLiveAgentHarnessSession, + releaseLiveAgentHarnessSession: mocks.releaseLiveAgentHarnessSession, + resolveAgentHarnessApproval: mocks.resolveAgentHarnessApproval, +})); + +const { startAgentHarnessApprovalRun, startAgentHarnessRun } = + await import("./runner.js"); describe("startAgentHarnessRun", () => { beforeEach(() => { @@ -31,6 +41,7 @@ describe("startAgentHarnessRun", () => { mocks.getAgentHarnessSession.mockResolvedValue({ pendingApproval: null }); mocks.updateAgentHarnessSession.mockResolvedValue({}); mocks.markAgentHarnessSessionStopped.mockResolvedValue({}); + mocks.resolveAgentHarnessApproval.mockResolvedValue({ ok: true }); }); it("streams harness events through startRun and detaches session state", async () => { @@ -108,6 +119,51 @@ describe("startAgentHarnessRun", () => { ); }); + it("continues an approval inside its own run-manager run", async () => { + let capturedRunFn: + | ((send: (event: AgentChatEvent) => void) => Promise) + | undefined; + mocks.startRun.mockImplementation((runId, threadId, runFn) => { + capturedRunFn = runFn; + return { + runId, + threadId, + turnId: "turn-1", + events: [], + status: "running", + subscribers: new Set(), + abort: new AbortController(), + startedAt: Date.now(), + }; + }); + mocks.resolveAgentHarnessApproval.mockImplementation( + async ({ onHarnessEvent }) => { + await onHarnessEvent({ type: "text-delta", text: "continued" }); + return { ok: true }; + }, + ); + + startAgentHarnessApprovalRun({ + runId: "approval-run", + harnessRunId: "original-run", + threadId: "thread-1", + turnId: "turn-1", + approval: { id: "approval-1", approved: true }, + scope: { ownerEmail: "alice@example.com", orgId: "org-1" }, + }); + const sent: AgentChatEvent[] = []; + await capturedRunFn?.((event) => sent.push(event)); + + expect(mocks.resolveAgentHarnessApproval).toHaveBeenCalledWith( + expect.objectContaining({ + runId: "original-run", + approval: { id: "approval-1", approved: true }, + scope: { ownerEmail: "alice@example.com", orgId: "org-1" }, + }), + ); + expect(sent).toEqual([{ type: "text", text: "continued" }]); + }); + it("stops and marks the session when the run signal is aborted", async () => { const session = fakeSession("native-2", [ { type: "text-delta", text: "hi" }, diff --git a/packages/core/src/agent/harness/runner.ts b/packages/core/src/agent/harness/runner.ts index 0dd1d6e5de..2ab1fb2477 100644 --- a/packages/core/src/agent/harness/runner.ts +++ b/packages/core/src/agent/harness/runner.ts @@ -4,6 +4,8 @@ import type { AgentChatEvent } from "../types.js"; import { registerLiveAgentHarnessSession, releaseLiveAgentHarnessSession, + resolveAgentHarnessApproval, + type AgentHarnessOwnerScope, } from "./lifecycle.js"; import { getAgentHarnessSession, @@ -14,6 +16,7 @@ import { import { agentHarnessEventToAgentChatEvents } from "./translate.js"; import type { AgentHarnessAdapter, + AgentHarnessApproval, AgentHarnessCreateSessionOptions, AgentHarnessEvent, AgentHarnessSession, @@ -36,6 +39,42 @@ export interface StartAgentHarnessRunOptions { onRunComplete?: (run: ActiveRun) => void | Promise; } +export interface StartAgentHarnessApprovalRunOptions { + runId: string; + harnessRunId: string; + threadId: string; + turnId?: string; + approval: AgentHarnessApproval; + scope: AgentHarnessOwnerScope; + runOptions?: StartRunOptions; + onRunComplete?: (run: ActiveRun) => void | Promise; +} + +export function startAgentHarnessApprovalRun( + opts: StartAgentHarnessApprovalRunOptions, +): ActiveRun { + return startRun( + opts.runId, + opts.threadId, + async (send) => { + const result = await resolveAgentHarnessApproval({ + runId: opts.harnessRunId, + approval: opts.approval, + scope: opts.scope, + onHarnessEvent: (event) => sendAgentHarnessEvent(send, event), + }); + if (!result.ok) { + throw new Error(result.error ?? "Harness approval failed"); + } + }, + opts.onRunComplete, + { + ...(opts.runOptions ?? {}), + turnId: opts.turnId ?? opts.runOptions?.turnId, + }, + ); +} + export function startAgentHarnessRun( opts: StartAgentHarnessRunOptions, ): ActiveRun { diff --git a/packages/core/src/agent/harness/store.ts b/packages/core/src/agent/harness/store.ts index 2407257b32..e200e449f0 100644 --- a/packages/core/src/agent/harness/store.ts +++ b/packages/core/src/agent/harness/store.ts @@ -283,25 +283,44 @@ export async function getAgentHarnessSession( return rowToHarnessSession(rows[0]); } +export interface AgentHarnessSessionScope { + ownerEmail: string | null; + orgId?: string | null; +} + export async function getLatestAgentHarnessSessionForThread( threadId: string, harnessName?: string, + scope?: AgentHarnessSessionScope, ): Promise { await ensureAgentHarnessSessionTables(); const client = getDbExec(); - const { rows } = harnessName - ? await client.execute({ - sql: `SELECT * FROM agent_harness_sessions - WHERE thread_id = ? AND harness_name = ? AND status != 'destroyed' - ORDER BY updated_at DESC LIMIT 1`, - args: [threadId, harnessName], - }) - : await client.execute({ - sql: `SELECT * FROM agent_harness_sessions - WHERE thread_id = ? AND status != 'destroyed' - ORDER BY updated_at DESC LIMIT 1`, - args: [threadId], - }); + const clauses = ["thread_id = ?", "status != 'destroyed'"]; + const args: unknown[] = [threadId]; + if (harnessName) { + clauses.push("harness_name = ?"); + args.push(harnessName); + } + if (scope) { + if (scope.ownerEmail === null) clauses.push("owner_email IS NULL"); + else { + clauses.push("owner_email = ?"); + args.push(scope.ownerEmail); + } + if (scope.orgId !== undefined) { + if (scope.orgId === null) clauses.push("org_id IS NULL"); + else { + clauses.push("org_id = ?"); + args.push(scope.orgId); + } + } + } + const { rows } = await client.execute({ + sql: `SELECT * FROM agent_harness_sessions + WHERE ${clauses.join(" AND ")} + ORDER BY updated_at DESC LIMIT 1`, + args, + }); return rowToHarnessSession(rows[0]); } diff --git a/packages/core/src/agent/harness/translate.spec.ts b/packages/core/src/agent/harness/translate.spec.ts index 0d0b8898de..571b8ea645 100644 --- a/packages/core/src/agent/harness/translate.spec.ts +++ b/packages/core/src/agent/harness/translate.spec.ts @@ -25,16 +25,18 @@ describe("agentHarnessEventToAgentChatEvents", () => { ).toEqual([{ type: "activity", label: "Starting", tool: "harness" }]); }); - it("normalizes tool input and result payloads", () => { + it("normalizes tool input and preserves tool ids", () => { expect( agentHarnessEventToAgentChatEvents({ type: "tool-start", + id: "tool-1", name: "read_file", input: { path: "app.tsx", options: { lines: 20 } }, }), ).toEqual([ { type: "tool_start", + id: "tool-1", tool: "read_file", input: { path: "app.tsx", options: '{\n "lines": 20\n}' }, }, @@ -43,6 +45,7 @@ describe("agentHarnessEventToAgentChatEvents", () => { expect( agentHarnessEventToAgentChatEvents({ type: "tool-done", + id: "tool-1", name: "read_file", input: { path: "app.tsx", options: { lines: 20 } }, result: { ok: true }, @@ -50,6 +53,7 @@ describe("agentHarnessEventToAgentChatEvents", () => { ).toEqual([ { type: "tool_done", + id: "tool-1", tool: "read_file", input: { path: "app.tsx", options: '{\n "lines": 20\n}' }, result: '{\n "ok": true\n}', @@ -57,6 +61,26 @@ describe("agentHarnessEventToAgentChatEvents", () => { ]); }); + it("maps harness approvals to the native approval contract", () => { + expect( + agentHarnessEventToAgentChatEvents({ + type: "approval-request", + id: "approval-1", + tool: "write_file", + message: "Allow this write?", + input: { path: "app.tsx" }, + }), + ).toEqual([ + { + type: "approval_required", + tool: "write_file", + input: { path: "app.tsx" }, + approvalKey: "approval-1", + toolCallId: "approval-1", + }, + ]); + }); + it("projects harness lifecycle events into activities", () => { expect( agentHarnessEventToAgentChatEvents({ diff --git a/packages/core/src/agent/harness/translate.ts b/packages/core/src/agent/harness/translate.ts index b92f5d0653..a49922b660 100644 --- a/packages/core/src/agent/harness/translate.ts +++ b/packages/core/src/agent/harness/translate.ts @@ -23,6 +23,7 @@ export function agentHarnessEventToAgentChatEvents( return [ { type: "tool_start", + ...(event.id ? { id: event.id } : {}), tool: event.name, input: normalizeToolInput(event.input), }, @@ -31,6 +32,7 @@ export function agentHarnessEventToAgentChatEvents( return [ { type: "tool_done", + ...(event.id ? { id: event.id } : {}), tool: event.name, ...(event.input !== undefined ? { input: normalizeToolInput(event.input) } @@ -42,9 +44,11 @@ export function agentHarnessEventToAgentChatEvents( case "approval-request": return [ { - type: "activity", - label: event.message || "Waiting for harness approval", - ...(event.tool ? { tool: event.tool } : {}), + type: "approval_required", + tool: event.tool ?? "harness", + input: normalizeToolInput(event.input), + approvalKey: event.id, + toolCallId: event.id, }, ]; case "file-change": diff --git a/packages/core/src/agent/harness/types.ts b/packages/core/src/agent/harness/types.ts index e6f7ff9c3d..03c6a9b7ce 100644 --- a/packages/core/src/agent/harness/types.ts +++ b/packages/core/src/agent/harness/types.ts @@ -33,6 +33,24 @@ export interface AgentHarnessAdapter { ): Promise; } +export interface AgentHarnessHostToolContext { + toolCallId: string; + abortSignal?: AbortSignal; + /** Set only by an adapter after its human-approval flow authorized this call. */ + approved?: boolean; +} + +export interface AgentHarnessHostTool { + description: string; + inputSchema: Record; + readOnly?: boolean; + needsApproval?: (input: unknown) => boolean | Promise; + execute( + input: unknown, + context: AgentHarnessHostToolContext, + ): unknown | Promise; +} + export interface AgentHarnessCreateSessionOptions { /** Agent Native session id. Adapters may map this to a native runtime id. */ sessionId?: string; @@ -41,7 +59,7 @@ export interface AgentHarnessCreateSessionOptions { cwd?: string; instructions?: string; skills?: unknown[]; - tools?: Record; + tools?: Record; permissionMode?: AgentHarnessPermissionMode; sandbox?: unknown; /** diff --git a/packages/core/src/server/agent-chat-plugin.surface.spec.ts b/packages/core/src/server/agent-chat-plugin.surface.spec.ts index f1caa751ca..b28fb410ae 100644 --- a/packages/core/src/server/agent-chat-plugin.surface.spec.ts +++ b/packages/core/src/server/agent-chat-plugin.surface.spec.ts @@ -80,6 +80,23 @@ describe("lean production run policy", () => { }); }); +describe("app chat harness wiring", () => { + it("selects the dedicated harness handler without passing it to AgentEngine", () => { + const source = readFileSync("src/server/agent-chat-plugin.ts", { + encoding: "utf-8", + }); + + expect(source).toContain("const harnessHandler = options?.harness"); + expect(source).toContain("harnessHandler ??"); + expect( + source.indexOf("ownerContext.anonymous && anonymousHandler"), + ).toBeLessThan( + source.indexOf("harnessHandler ??", source.indexOf("const handler =")), + ); + expect(source).not.toMatch(/engineOption:\s*options\?\.harness/); + }); +}); + describe("interactive agent run options", () => { it("forwards an app's durable no-progress watchdog to every interactive handler", () => { expect( diff --git a/packages/core/src/server/agent-chat-plugin.ts b/packages/core/src/server/agent-chat-plugin.ts index c0442846b1..1108cbca67 100644 --- a/packages/core/src/server/agent-chat-plugin.ts +++ b/packages/core/src/server/agent-chat-plugin.ts @@ -303,6 +303,10 @@ import { generateActionsPrompt, generateCorpusToolsPrompt, } from "./agent-chat/framework-prompts.js"; +import { + createAgentHarnessChatHandler, + type AgentChatHarnessConfig, +} from "./agent-chat/harness-handler.js"; import { type AgentChatPluginOptions, type NitroPluginDef, @@ -334,7 +338,7 @@ export { loadResourcesForPrompt }; export { _agentChatPromptSectionsForTests }; export { buildPublicAgentA2ASkills }; export { assembleA2AFinalResponse }; -export type { AgentChatPluginOptions }; +export type { AgentChatHarnessConfig, AgentChatPluginOptions }; export { runA2AAgentLoop }; export { runMCPAgentLoop }; export { createA2AEngineToolSurface }; @@ -3106,6 +3110,73 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su resolveOwnerEmail: isHostedProd ? getOwnerFromEvent : undefined, }); + const harnessHandler = options?.harness + ? createAgentHarnessChatHandler({ + harness: options.harness, + actions: leanPrompt ? leanActions : prodActions, + resolveOwnerEmail: getOwnerFromEvent, + resolveOrgId: getOrgIdFromEvent, + systemPrompt: async (event) => { + const { owner, extra } = await prepareRun(event); + const runtimeContext = runtimeContextForEvent(event); + const codeEditingSurfaceRestriction = + shouldBlockInProductCodeEditing(event) + ? APP_RENDERED_CHAT_NO_DIRECT_CODE_PROMPT + : ""; + if (leanPrompt) { + return ( + leanBasePrompt + + buildLeanRunPolicyPrompt( + codeEditingSurfaceRestriction, + prodCodeExecPromptNote, + ) + + extra + + runtimeContext + ); + } + const resources = await loadResourcesForPrompt( + owner, + lazyContext, + options?.appId, + ); + const schemaBlock = lazyContext + ? "" + : await buildSchemaBlock(owner, databaseToolsMode); + return ( + basePrompt + + resources + + schemaBlock + + codeEditingSurfaceRestriction + + prodCodeExecPromptNote + + extra + + runtimeContext + ); + }, + prepareRequest: async (details) => { + if (details.threadId) { + const existingThread = await getThread(details.threadId); + if (existingThread) { + const access = await resolveThreadAccess( + details.ownerEmail, + details.threadId, + "editor", + { orgId: await getOrgIdFromEvent(details.event) }, + ); + if (!access) { + throw createError({ + statusCode: 404, + statusMessage: "Thread not found", + }); + } + } + } + return options?.prepareRequest?.(details); + }, + onRunPrepared: persistSubmittedUserMessage, + onRunComplete, + }) + : null; + const anonymousHandler = options?.anonymousOwner && options.anonymousReadOnly !== false ? createProductionAgentHandler({ @@ -5362,9 +5433,10 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su const handler = ownerContext.anonymous && anonymousHandler ? anonymousHandler - : !blockInProductCodeEditing && currentDevMode && devHandler - ? devHandler - : prodHandler; + : (harnessHandler ?? + (!blockInProductCodeEditing && currentDevMode && devHandler + ? devHandler + : prodHandler)); return handler(event); }, ); diff --git a/packages/core/src/server/agent-chat/harness-handler.spec.ts b/packages/core/src/server/agent-chat/harness-handler.spec.ts new file mode 100644 index 0000000000..7b5f0d4afb --- /dev/null +++ b/packages/core/src/server/agent-chat/harness-handler.spec.ts @@ -0,0 +1,329 @@ +import { mockEvent } from "h3"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import type { AgentHarnessAdapter } from "../../agent/harness/types.js"; + +const mocks = vi.hoisted(() => ({ + registerBuiltins: vi.fn(), + resolveAdapter: vi.fn(), + getLatest: vi.fn(), + startHarnessRun: vi.fn(), + startApprovalRun: vi.fn(), + resolveApproval: vi.fn(), + getActive: vi.fn(), + subscribe: vi.fn(), + tryClaim: vi.fn(), +})); + +vi.mock("../../agent/harness/index.js", () => ({ + registerBuiltinAgentHarnesses: mocks.registerBuiltins, + resolveAgentHarness: mocks.resolveAdapter, + getLatestAgentHarnessSessionForThread: mocks.getLatest, + startAgentHarnessRun: mocks.startHarnessRun, + startAgentHarnessApprovalRun: mocks.startApprovalRun, + resolveAgentHarnessApproval: mocks.resolveApproval, +})); +vi.mock("../../agent/run-manager.js", () => ({ + getActiveRunForThread: mocks.getActive, + subscribeToRun: mocks.subscribe, +})); +vi.mock("../../agent/run-store.js", () => ({ + tryClaimRunSlot: mocks.tryClaim, +})); + +const { + createAgentHarnessChatHandler, + evaluateAgentChatHarnessGuard, + resolveAgentChatHarnessAdapter, +} = await import("./harness-handler.js"); + +const adapter: AgentHarnessAdapter = { + name: "fake", + label: "Fake", + description: "Fake harness", + capabilities: { + sandbox: false, + resumable: true, + approvals: true, + hostTools: false, + fileEvents: false, + }, + createSession: vi.fn(), +}; + +const activeRun = { + runId: "run-1", + threadId: "thread-1", + turnId: "turn-1", + events: [], + status: "running", + subscribers: new Set(), + abort: new AbortController(), + startedAt: Date.now(), +}; + +describe("agent chat harness configuration", () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.resolveAdapter.mockReturnValue(adapter); + mocks.getLatest.mockResolvedValue(null); + mocks.getActive.mockReturnValue(null); + mocks.resolveApproval.mockResolvedValue({ ok: true, runId: "prior-run" }); + mocks.tryClaim.mockResolvedValue({ claimed: true, activeRunId: null }); + mocks.startHarnessRun.mockReturnValue(activeRun); + mocks.startApprovalRun.mockReturnValue(activeRun); + mocks.subscribe.mockReturnValue({ stream: true }); + }); + + it("accepts an adapter instance without resolving an engine", () => { + expect(resolveAgentChatHarnessAdapter({ adapter }, vi.fn())).toBe(adapter); + }); + + it("resolves a registered adapter with its config", () => { + const resolve = vi.fn(() => adapter); + + expect( + resolveAgentChatHarnessAdapter( + { adapter: { name: "fake", config: { mode: "test" } } }, + resolve, + ), + ).toBe(adapter); + expect(resolve).toHaveBeenCalledWith("fake", { mode: "test" }); + }); + + it("fails closed when the request guard denies access", async () => { + const event = mockEvent(new Request("http://localhost/chat")); + + await expect( + evaluateAgentChatHarnessGuard( + { + adapter, + guard: async () => ({ + allowed: false, + status: 403, + error: "Local harness access denied", + }), + }, + event, + ), + ).resolves.toEqual({ + allowed: false, + status: 403, + error: "Local harness access denied", + }); + }); + + it("does not resolve or start a runtime after a guard denial", async () => { + const handler = createAgentHarnessChatHandler({ + harness: { adapter: "fake", guard: () => false }, + resolveOwnerEmail: () => "owner@example.com", + }); + const event = chatEvent({ message: "hello", threadId: "thread-1" }); + + await expect(handler(event)).resolves.toEqual({ + error: "Agent harness access denied", + }); + expect(mocks.resolveAdapter).not.toHaveBeenCalled(); + expect(mocks.startHarnessRun).not.toHaveBeenCalled(); + }); + + it("starts a foreground harness run with scoped resume state and no replayed history", async () => { + mocks.getLatest.mockResolvedValue({ + id: "session-1", + runId: "prior-run", + harnessName: "fake", + threadId: "thread-1", + providerSessionId: "native-1", + status: "idle", + resumeState: { cursor: "opaque" }, + ownerEmail: "owner@example.com", + orgId: "org-1", + resolvedApprovalIds: [], + createdAt: Date.now(), + updatedAt: Date.now(), + }); + const onRunPrepared = vi.fn(); + const handler = createAgentHarnessChatHandler({ + harness: { adapter: "fake", permissionMode: "allow-reads" }, + resolveOwnerEmail: () => "owner@example.com", + resolveOrgId: () => "org-1", + systemPrompt: "Use app actions.", + onRunPrepared, + }); + const event = chatEvent({ + message: "new prompt", + history: [{ role: "user", content: "must not replay" }], + threadId: "thread-1", + turnId: "turn-1", + }); + + await expect(handler(event)).resolves.toEqual({ stream: true }); + expect(mocks.getLatest).toHaveBeenCalledWith("thread-1", "fake", { + ownerEmail: "owner@example.com", + orgId: "org-1", + }); + expect(mocks.startHarnessRun).toHaveBeenCalledWith( + expect.objectContaining({ + threadId: "thread-1", + turnId: "turn-1", + adapter, + input: { + prompt: "new prompt", + metadata: undefined, + }, + createSession: expect.objectContaining({ + sessionId: "session-1", + resumeState: { cursor: "opaque" }, + permissionMode: "allow-reads", + instructions: "Use app actions.", + }), + ownerEmail: "owner@example.com", + orgId: "org-1", + }), + ); + expect(onRunPrepared).toHaveBeenCalledOnce(); + }); + + it("resolves a live pending approval without replacing its active run", async () => { + mocks.getLatest.mockResolvedValue({ + id: "session-1", + runId: "prior-run", + harnessName: "fake", + threadId: "thread-1", + providerSessionId: "native-1", + status: "running", + pendingApproval: { + type: "approval-request", + id: "approval-live", + message: "Allow?", + }, + ownerEmail: "owner@example.com", + orgId: null, + resolvedApprovalIds: [], + createdAt: Date.now(), + updatedAt: Date.now(), + }); + mocks.getActive.mockReturnValue({ ...activeRun, runId: "prior-run" }); + const handler = createAgentHarnessChatHandler({ + harness: { adapter: "fake" }, + resolveOwnerEmail: () => "owner@example.com", + }); + + await expect( + handler( + chatEvent({ + message: "", + threadId: "thread-1", + approvedToolCalls: ["approval-live"], + }), + ), + ).resolves.toEqual({ stream: true }); + expect(mocks.resolveApproval).toHaveBeenCalledWith({ + runId: "prior-run", + approval: { id: "approval-live", approved: true }, + scope: { ownerEmail: "owner@example.com", orgId: null }, + }); + expect(mocks.startApprovalRun).not.toHaveBeenCalled(); + expect(mocks.tryClaim).not.toHaveBeenCalled(); + }); + + it("continues only the matching idle approval through a run-manager run", async () => { + mocks.getLatest.mockResolvedValue({ + id: "session-1", + runId: "prior-run", + harnessName: "fake", + threadId: "thread-1", + providerSessionId: "native-1", + status: "idle", + pendingApproval: { + type: "approval-request", + id: "approval-1", + message: "Allow?", + }, + ownerEmail: "owner@example.com", + orgId: null, + resolvedApprovalIds: [], + createdAt: Date.now(), + updatedAt: Date.now(), + }); + const handler = createAgentHarnessChatHandler({ + harness: { adapter: "fake" }, + resolveOwnerEmail: () => "owner@example.com", + }); + const event = chatEvent({ + message: "", + threadId: "thread-1", + approvedToolCalls: ["approval-1"], + }); + + await expect(handler(event)).resolves.toEqual({ stream: true }); + expect(mocks.startApprovalRun).toHaveBeenCalledWith( + expect.objectContaining({ + harnessRunId: "prior-run", + threadId: "thread-1", + approval: { id: "approval-1", approved: true }, + scope: { ownerEmail: "owner@example.com", orgId: null }, + }), + ); + expect(mocks.startHarnessRun).not.toHaveBeenCalled(); + }); + + it("forces Plan mode onto read-only permissions and guarded action tools", async () => { + const handler = createAgentHarnessChatHandler({ + harness: { adapter: "fake", permissionMode: "allow-all" }, + actions: { + updateRecord: { + tool: { + description: "Update a record", + parameters: { type: "object", properties: {} }, + }, + run: vi.fn(async () => "updated"), + }, + }, + resolveOwnerEmail: () => "owner@example.com", + }); + + await expect( + handler(chatEvent({ message: "plan this", mode: "plan" })), + ).resolves.toEqual({ stream: true }); + const call = mocks.startHarnessRun.mock.calls.at(-1)?.[0]; + expect(call.createSession.permissionMode).toBe("allow-reads"); + expect(call.createSession.tools.updateRecord.description).toContain( + "Plan mode blocked", + ); + }); + + it("rejects attachments before persisting or starting a harness run", async () => { + const onRunPrepared = vi.fn(); + const handler = createAgentHarnessChatHandler({ + harness: { adapter: "fake" }, + resolveOwnerEmail: () => "owner@example.com", + onRunPrepared, + }); + + await expect( + handler( + chatEvent({ + message: "read this", + attachments: [ + { type: "text", name: "notes.txt", text: "private notes" }, + ], + }), + ), + ).resolves.toEqual({ + error: "This agent harness does not support chat attachments", + }); + expect(onRunPrepared).not.toHaveBeenCalled(); + expect(mocks.startHarnessRun).not.toHaveBeenCalled(); + }); +}); + +function chatEvent(body: Record) { + return mockEvent( + new Request("http://localhost/_agent-native/agent-chat", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }), + ); +} diff --git a/packages/core/src/server/agent-chat/harness-handler.ts b/packages/core/src/server/agent-chat/harness-handler.ts new file mode 100644 index 0000000000..ecb85b10e2 --- /dev/null +++ b/packages/core/src/server/agent-chat/harness-handler.ts @@ -0,0 +1,406 @@ +import { randomUUID } from "node:crypto"; + +import { + defineEventHandler, + getMethod, + readBody, + setResponseHeader, + setResponseStatus, + type EventHandler, + type H3Event, +} from "h3"; + +import { createAgentHarnessActionTools } from "../../agent/harness/chat-tools.js"; +import { + getLatestAgentHarnessSessionForThread, + registerBuiltinAgentHarnesses, + resolveAgentHarness, + resolveAgentHarnessApproval, + startAgentHarnessApprovalRun, + startAgentHarnessRun, + type AgentHarnessAdapter, + type AgentHarnessCreateSessionOptions, + type AgentHarnessPermissionMode, +} from "../../agent/harness/index.js"; +import { + createPlanModeActionRegistry, + type ActionEntry, +} from "../../agent/production-agent.js"; +import { + getActiveRunForThread, + subscribeToRun, +} from "../../agent/run-manager.js"; +import { tryClaimRunSlot } from "../../agent/run-store.js"; +import type { + AgentChatAttachment, + AgentChatReference, + AgentChatRequest, +} from "../../agent/types.js"; + +export type AgentChatHarnessGuardDecision = + | boolean + | { + allowed: boolean; + status?: number; + error?: string; + }; + +export interface AgentChatHarnessConfig { + /** A full harness adapter, or a registered adapter name/config. Never an AgentEngine. */ + adapter: + | AgentHarnessAdapter + | string + | { name: string; config?: Record }; + /** Per-request authorization boundary. A denial fails closed without engine fallback. */ + guard?: ( + event: H3Event, + ) => AgentChatHarnessGuardDecision | Promise; + permissionMode?: AgentHarnessPermissionMode; + cwd?: string | ((event: H3Event) => string | Promise); + createSession?: Omit< + AgentHarnessCreateSessionOptions, + "sessionId" | "threadId" | "runId" | "resumeState" | "signal" + >; + instructions?: string | ((event: H3Event) => string | Promise); + metadata?: Record; +} + +export interface AgentHarnessChatHandlerOptions { + harness: AgentChatHarnessConfig; + actions?: Record; + resolveOwnerEmail: (event: H3Event) => string | null | Promise; + resolveOrgId?: ( + event: H3Event, + ) => string | null | undefined | Promise; + systemPrompt?: string | ((event: H3Event) => string | Promise); + prepareRequest?: (details: { + event: H3Event; + ownerEmail: string; + message: string; + displayMessage?: string; + attachments: AgentChatAttachment[]; + references: AgentChatReference[]; + threadId?: string; + internalContinuation?: boolean; + mode: "act" | "plan"; + }) => + | void + | { + message?: string; + displayMessage?: string; + attachments?: AgentChatAttachment[]; + } + | Promise; + onRunPrepared?: (details: { + runId: string; + threadId: string | undefined; + message: string; + attachments?: AgentChatAttachment[]; + }) => void | Promise; + onRunComplete?: ( + run: import("../../agent/run-manager.js").ActiveRun, + threadId: string | undefined, + ) => void | Promise; +} + +const ALLOWED_GUARD_STATUS = new Set([401, 403, 404, 409, 503]); + +export function resolveAgentChatHarnessAdapter( + config: Pick, + resolve: typeof resolveAgentHarness = resolveAgentHarness, +): AgentHarnessAdapter { + if (typeof config.adapter === "object" && "createSession" in config.adapter) { + return config.adapter; + } + registerBuiltinAgentHarnesses(); + if (typeof config.adapter === "string") return resolve(config.adapter); + return resolve(config.adapter.name, config.adapter.config); +} + +export async function evaluateAgentChatHarnessGuard( + config: AgentChatHarnessConfig, + event: H3Event, +): Promise< + { allowed: true } | { allowed: false; status: number; error: string } +> { + if (!config.guard) return { allowed: true }; + const decision = await config.guard(event); + if (decision === true || (typeof decision === "object" && decision.allowed)) { + return { allowed: true }; + } + const requestedStatus = + typeof decision === "object" ? decision.status : undefined; + return { + allowed: false, + status: + requestedStatus && ALLOWED_GUARD_STATUS.has(requestedStatus) + ? requestedStatus + : 403, + error: + typeof decision === "object" && decision.error + ? decision.error + : "Agent harness access denied", + }; +} + +export function createAgentHarnessChatHandler( + options: AgentHarnessChatHandlerOptions, +): EventHandler { + return defineEventHandler(async (event) => { + if (getMethod(event) !== "POST") { + setResponseStatus(event, 405); + return { error: "Method not allowed" }; + } + + const guard = await evaluateAgentChatHarnessGuard(options.harness, event); + if (!guard.allowed) { + setResponseStatus(event, guard.status); + return { error: guard.error }; + } + + let body: AgentChatRequest; + try { + const parsed = await readBody(event); + if (!parsed || typeof parsed !== "object") + throw new Error("invalid body"); + body = parsed; + } catch { + setResponseStatus(event, 400); + return { error: "Invalid request body" }; + } + if (body.__backgroundRun) { + setResponseStatus(event, 503); + return { + error: "Agent harness chat requires a foreground runtime", + }; + } + + const ownerEmail = await options.resolveOwnerEmail(event); + if (!ownerEmail) { + setResponseStatus(event, 401); + return { error: "Authentication required" }; + } + const orgId = (await options.resolveOrgId?.(event)) ?? null; + const threadId = body.threadId; + const effectiveThreadId = threadId ?? randomUUID(); + const turnId = body.turnId ?? randomUUID(); + const runId = randomUUID(); + const requestMode = body.mode === "plan" ? "plan" : "act"; + const adapter = resolveAgentChatHarnessAdapter(options.harness); + const latest = await getLatestAgentHarnessSessionForThread( + effectiveThreadId, + adapter.name, + { ownerEmail, orgId }, + ); + const pendingApproval = pendingApprovalId(latest?.pendingApproval); + const approved = body.approvedToolCalls?.find( + (approvalKey) => approvalKey === pendingApproval, + ); + + if (approved && latest?.runId) { + const activeRun = getActiveRunForThread(effectiveThreadId); + if (activeRun?.runId === latest.runId) { + const result = await resolveAgentHarnessApproval({ + runId: latest.runId, + approval: { id: approved, approved: true }, + scope: { ownerEmail, orgId }, + }); + if (!result.ok) { + setResponseStatus(event, 409); + return { error: result.error ?? "Harness approval failed" }; + } + return streamHarnessRun(event, activeRun.runId); + } + if (activeRun) { + setResponseStatus(event, 409); + return { + error: "Run already in progress for this thread", + activeRunId: activeRun.runId, + }; + } + const run = startAgentHarnessApprovalRun({ + runId, + harnessRunId: latest.runId, + threadId: effectiveThreadId, + turnId, + approval: { id: approved, approved: true }, + scope: { ownerEmail, orgId }, + onRunComplete: options.onRunComplete + ? (completed) => options.onRunComplete!(completed, threadId) + : undefined, + }); + return streamHarnessRun(event, run.runId); + } + if (pendingApproval) { + setResponseStatus(event, 409); + return { + error: "Resolve the pending harness approval before continuing", + approvalKey: pendingApproval, + }; + } + + const hasMessage = + typeof body.message === "string" && body.message.trim().length > 0; + const attachments = Array.isArray(body.attachments) ? body.attachments : []; + if (attachments.length > 0) { + setResponseStatus(event, 400); + return { + error: "This agent harness does not support chat attachments", + }; + } + if (!hasMessage) { + setResponseStatus(event, 400); + return { error: "message is required" }; + } + + let message = body.message; + let preparedAttachments = attachments; + const prepared = await options.prepareRequest?.({ + event, + ownerEmail, + message, + displayMessage: body.displayMessage, + attachments, + references: Array.isArray(body.references) ? body.references : [], + threadId, + internalContinuation: Boolean(body.internalContinuation), + mode: requestMode, + }); + if (prepared?.message?.trim()) message = prepared.message; + if (prepared?.attachments) preparedAttachments = prepared.attachments; + if (preparedAttachments.length > 0) { + setResponseStatus(event, 400); + return { + error: "This agent harness does not support chat attachments", + }; + } + if (!message.trim()) { + setResponseStatus(event, 400); + return { error: "message is required" }; + } + + const configuredInstructions = await resolveValue( + options.harness.instructions, + event, + ); + const systemPrompt = await resolveValue(options.systemPrompt, event); + const configuredCwd = await resolveValue(options.harness.cwd, event); + const requestActions = options.actions + ? requestMode === "plan" + ? createPlanModeActionRegistry(options.actions) + : options.actions + : undefined; + const actionTools = requestActions + ? createAgentHarnessActionTools({ + actions: requestActions, + ownerEmail, + orgId, + threadId: effectiveThreadId, + turnId, + }) + : {}; + const createSession: AgentHarnessCreateSessionOptions = { + ...(options.harness.createSession ?? {}), + sessionId: latest?.id ?? randomUUID(), + resumeState: latest?.resumeState, + ...(configuredCwd ? { cwd: configuredCwd } : {}), + ...(systemPrompt || configuredInstructions + ? { + instructions: [systemPrompt, configuredInstructions] + .filter(Boolean) + .join("\n\n"), + } + : {}), + tools: { + ...(options.harness.createSession?.tools ?? {}), + ...actionTools, + }, + permissionMode: + requestMode === "plan" + ? "allow-reads" + : (options.harness.permissionMode ?? + options.harness.createSession?.permissionMode ?? + "allow-reads"), + metadata: { + ...(options.harness.createSession?.metadata ?? {}), + ...(options.harness.metadata ?? {}), + }, + }; + + if (threadId) { + const activeRun = getActiveRunForThread(threadId); + if (activeRun) { + setResponseStatus(event, 409); + return { + error: "Run already in progress for this thread", + activeRunId: activeRun.runId, + }; + } + const slot = await tryClaimRunSlot(threadId); + if (!slot.claimed) { + setResponseStatus(event, 409); + return { + error: "Run already in progress for this thread", + activeRunId: slot.activeRunId, + }; + } + } + + await options.onRunPrepared?.({ + runId, + threadId, + message: body.displayMessage ?? message, + }); + + const run = startAgentHarnessRun({ + runId, + threadId: effectiveThreadId, + turnId, + adapter, + input: { + prompt: message, + metadata: options.harness.metadata, + }, + createSession, + ownerEmail, + orgId, + onRunComplete: options.onRunComplete + ? (completed) => options.onRunComplete!(completed, threadId) + : undefined, + }); + return streamHarnessRun(event, run.runId); + }); +} + +async function resolveValue( + value: string | ((event: H3Event) => string | Promise) | undefined, + event: H3Event, +): Promise { + return typeof value === "function" ? value(event) : value; +} + +function pendingApprovalId(value: unknown): string | null { + if (!value || typeof value !== "object") return null; + const record = value as { type?: unknown; id?: unknown }; + return record.type === "approval-request" && typeof record.id === "string" + ? record.id + : null; +} + +function streamHarnessRun(event: H3Event, runId: string) { + const stream = subscribeToRun(runId, 0); + if (!stream) { + setResponseStatus(event, 500); + return { error: "Failed to start agent harness run" }; + } + setResponseHeader(event, "Content-Type", "text/event-stream"); + setResponseHeader(event, "Cache-Control", "no-cache"); + setResponseHeader(event, "Connection", "keep-alive"); + setResponseHeader(event, "X-Run-Id", runId); + setResponseHeader(event, "X-Dispatch-Mode", "foreground"); + return stream; +} diff --git a/packages/core/src/server/agent-chat/plugin-options.ts b/packages/core/src/server/agent-chat/plugin-options.ts index 09ca76267a..cbf17c1b0e 100644 --- a/packages/core/src/server/agent-chat/plugin-options.ts +++ b/packages/core/src/server/agent-chat/plugin-options.ts @@ -8,6 +8,7 @@ import type { import type { ExternalAgentPolicy } from "../../mcp/external-agent-policy.js"; import type { DatabaseToolsOption } from "../../scripts/db/tool-mode.js"; import type { PromptExamples } from "../prompts/index.js"; +import type { AgentChatHarnessConfig } from "./harness-handler.js"; /** Shape of a Nitro plugin function: receives the Nitro app instance at * startup and may register routes/hooks synchronously or asynchronously. */ @@ -58,6 +59,13 @@ export interface AgentChatPluginOptions { | import("../../agent/engine/types.js").AgentEngine | string | { name: string; config: Record }; + /** + * Optional full agent harness for app chat. Harnesses own their loop and + * session lifecycle; they are never passed to AgentEngine or runAgentLoop. + * A configured harness fails closed when its request guard denies access or + * its adapter cannot be loaded. + */ + harness?: AgentChatHarnessConfig; /** Route path. Default: /_agent-native/agent-chat */ path?: string; /** Custom mention providers for @-tagging template entities */ diff --git a/packages/core/src/server/index.ts b/packages/core/src/server/index.ts index d894b3f186..c0dc894813 100644 --- a/packages/core/src/server/index.ts +++ b/packages/core/src/server/index.ts @@ -198,6 +198,7 @@ export { createAgentChatPlugin, defaultAgentChatPlugin, refreshGlobalMcpManager, + type AgentChatHarnessConfig, type AgentChatPluginOptions, } from "./agent-chat-plugin.js"; export { From f4d87e8e6c0da4a91befa8c35d37a03f29a1566c Mon Sep 17 00:00:00 2001 From: RogierKonings Date: Wed, 29 Jul 2026 15:43:43 +0200 Subject: [PATCH 02/10] chore(core): prepare local Pi prerelease --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 6c07fd6f9b..96381e2f2b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@agent-native/core", - "version": "0.129.1", + "version": "0.130.0-local-pi.0", "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context", "homepage": "https://github.com/BuilderIO/agent-native#readme", "bugs": { From 59997c8fbc5decfccbe55ea13e40694cd4d4cfed Mon Sep 17 00:00:00 2001 From: RogierKonings Date: Wed, 29 Jul 2026 15:46:05 +0200 Subject: [PATCH 03/10] Revert "chore(core): prepare local Pi prerelease" This reverts commit f4d87e8e6c0da4a91befa8c35d37a03f29a1566c. --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 96381e2f2b..6c07fd6f9b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@agent-native/core", - "version": "0.130.0-local-pi.0", + "version": "0.129.1", "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context", "homepage": "https://github.com/BuilderIO/agent-native#readme", "bugs": { From f8144c6ea69d48c4d25957b326b3f5c340d65f27 Mon Sep 17 00:00:00 2001 From: RogierKonings Date: Wed, 29 Jul 2026 15:48:21 +0200 Subject: [PATCH 04/10] fix(core): declare local Pi as optional peer --- packages/core/package.json | 4 + pnpm-lock.yaml | 696 +++++++++++++++++++++++++++++++++++++ 2 files changed, 700 insertions(+) diff --git a/packages/core/package.json b/packages/core/package.json index 6c07fd6f9b..27992c8a68 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -460,6 +460,7 @@ "@ai-sdk/groq": ">=3", "@ai-sdk/mistral": ">=3", "@ai-sdk/openai": ">=3", + "@earendil-works/pi-coding-agent": ">=0.82.1", "@excalidraw/excalidraw": ">=0.18", "@excalidraw/mermaid-to-excalidraw": ">=2", "@openrouter/ai-sdk-provider": ">=2", @@ -497,6 +498,9 @@ "@ai-sdk/openai": { "optional": true }, + "@earendil-works/pi-coding-agent": { + "optional": true + }, "@openrouter/ai-sdk-provider": { "optional": true }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc88afd38b..2fb928372f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -767,6 +767,9 @@ importers: '@codemirror/theme-one-dark': specifier: ^6.1.3 version: 6.1.3 + '@earendil-works/pi-coding-agent': + specifier: '>=0.82.1' + version: 0.82.1(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.18.0)(zod@4.4.3) '@libsql/client': specifier: ^0.15.0 version: 0.15.15 @@ -6029,6 +6032,103 @@ packages: react: optional: true + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-bedrock-runtime@3.1048.0': + resolution: {integrity: sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.977.1': + resolution: {integrity: sha512-KVtQRtc00ES/y+Sc3vYXeP6pCIcNlBJCZOwvqSy8ZpVGmbM5+IG+AfhuTKQ2oXmIVqZJewaGMMpzPkywC6xg0w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.62': + resolution: {integrity: sha512-BkDrk2cNjed31IKin/Oksb2ziF+gfuyRskFVuT4EU9Mep7M8Y/d8DJG4+anHme4Vuse7CwaEscwEfGyR6mzBhQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.64': + resolution: {integrity: sha512-Wj1FGK2IxY5EccQCvH+niTYhIvDoDujJf2CpRRgS3NpYNEgiFNVItNbJYQjINRlu7fG7jSsXkKV0UWKriEplrw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.973.7': + resolution: {integrity: sha512-2CefB8cCxDu52P24B8Ay93/cTT199bcSvNHQ8e2f4BjSCF83yErBnTIZEBo0VeIgCfmw+PJKFUXnlQWxm2dkug==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.69': + resolution: {integrity: sha512-gM3j0Ie9+FoLNTYODY+QWbg3vCRBc7mR9cRdntxTMkFYIrwfRmuucfavP6HNBlYSuaYww54TNJGej4GFgoPZAg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.73': + resolution: {integrity: sha512-VTzdbf8Ukjdb9yUubZzRI678CWZvKovhE8Nv3qihwhC187sRMGls+r9N8Wuht5q1xjKx2nmpS48ar8ppupjkCA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.62': + resolution: {integrity: sha512-zXYU9UWNL66gtMgNLhmxlrvEokuI7r6G2q7FRGu41Bya4iS30JLelUipJX9SV4zhyCPWJhI9Li54R1d9H8Tq6A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.973.6': + resolution: {integrity: sha512-DobZggy3K49xdCpjeyMou0FQhkoYbluVGNydL6D+lcxF8GoAsttFX0xnH5GmiQ89We5dB6TRpW+CD/VowBH6HQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.68': + resolution: {integrity: sha512-bq+yTt+uWJx60VVp/OIAX5xqUAu/K2Uc3eknWnWl+KtfcU2CQe0uNw6lySrn2t5GKHq7jsV0Z63HiBGVtzr/lg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/eventstream-handler-node@3.972.30': + resolution: {integrity: sha512-hJboPgIpq5+ADc++/B9TBqn65CXV21cZLGB8V5RBQbxkZ/rQ6qMfcxTnW/SvQlasX4jhaSG8B1wsVjhQyDrsnQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-eventstream@3.972.25': + resolution: {integrity: sha512-9SFbPzJDHHR5k6Q6KvXVas/veUm/TzNcNTFM2UhdXHZHpyIvI2lS+s4cxljw1BihGpVhsAkQDo/2nW7dHxpf4Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-websocket@3.972.44': + resolution: {integrity: sha512-MPjH/vT1UZc7RSdvP/bIZCJqQCOORei84D6a7dwBuvdwOIskTsQ2EczlTRFQu7yWpGMQr1x3xdpDRHjWlTH2Tw==} + engines: {node: '>= 14.0.0'} + + '@aws-sdk/nested-clients@3.997.36': + resolution: {integrity: sha512-b71Suv7L+DnhM0MsQHU4WO42I32kxLZi96PbVhZbxMYIoKnEZz3v+LSrG8fupAoA4cBSshCk1Dl/PeRz49qUSg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.42': + resolution: {integrity: sha512-DBV4naZP6HYBlAvPpoQzOP12Wvfou/5rN8yJPXjBTBylU5qwCbh/tXr2MddHoIjgoRkEl/eS+IljiUqvmwey1Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1048.0': + resolution: {integrity: sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1096.0': + resolution: {integrity: sha512-hdUS2hDppy3vkWeFl5y86RLNU6OWH2mQB09yOSsRefwhhGTSFPkaZvfLDD/9vFcvMzlr8QFQFw3fw2FtrurVQA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.974.2': + resolution: {integrity: sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.8': + resolution: {integrity: sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.37': + resolution: {integrity: sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.3.0': + resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} + engines: {node: '>=18.0.0'} + '@azure/abort-controller@2.1.2': resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} engines: {node: '>=18.0.0'} @@ -6826,6 +6926,24 @@ packages: '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@earendil-works/pi-agent-core@0.82.1': + resolution: {integrity: sha512-Z3kloziJIE2dmrisRckZX8zDca/gIv9/YdFAzeoqpHiLV2wsni6bL4hInNSjVKLbqT+4kqLIkph2JQLKvSepjg==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-ai@0.82.1': + resolution: {integrity: sha512-3WFYRhEp3lQB3444EhPMBcM7zSaEUE3eJgHOR7s4081NLqbw/FsWilIKWXSua0Gv3sRr7m9xMidR3pPDE7jI/A==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-coding-agent@0.82.1': + resolution: {integrity: sha512-zbkAhoIuDPMF3pKuja0ajZabrMWU29FUMV9A/XMXT/XC1yXs5xt6t6t13GogQFsDrDqbFP4DkZQO1w8rWRAzYA==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-tui@0.82.1': + resolution: {integrity: sha512-9yN8hALfKaxZq7n54EMxqhFCWnMi6LHkraMJ/1YjHiATq75XrI6XDMVppn9EDtiK7Fks8hUe1SDXUTrIvwRWfQ==} + engines: {node: '>=22.19.0'} + '@electron-internal/extract-zip@1.0.4': resolution: {integrity: sha512-Zr1Vs7E9tpCNhZHDAbFVXc2gEVCG9RqPDjrno5+bdgB6LRAuvgyMHJut4NCVyYwtAieapMzc3fiQ3CSTi75ARg==} engines: {node: '>=22.12.0'} @@ -8671,6 +8789,74 @@ packages: '@marijn/find-cluster-break@1.0.2': resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + '@mariozechner/clipboard-darwin-arm64@0.3.9': + resolution: {integrity: sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@mariozechner/clipboard-darwin-universal@0.3.9': + resolution: {integrity: sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==} + engines: {node: '>= 10'} + os: [darwin] + + '@mariozechner/clipboard-darwin-x64@0.3.9': + resolution: {integrity: sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + resolution: {integrity: sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + resolution: {integrity: sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + resolution: {integrity: sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + resolution: {integrity: sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + resolution: {integrity: sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + resolution: {integrity: sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + resolution: {integrity: sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@mariozechner/clipboard@0.3.9': + resolution: {integrity: sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==} + engines: {node: '>= 10'} + '@mediapipe/tasks-vision@0.10.17': resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==} @@ -8686,6 +8872,14 @@ packages: '@mermaid-js/parser@1.1.1': resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==} + '@mistralai/mistralai@2.2.6': + resolution: {integrity: sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@mixmark-io/domino@2.2.0': resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} @@ -9019,6 +9213,10 @@ packages: resolution: {integrity: sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==} engines: {node: '>=8.0.0'} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@opentelemetry/api@1.9.1': resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} @@ -12831,6 +13029,9 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@silvia-odwyer/photon-node@0.3.4': + resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==} + '@sinclair/typebox@0.27.10': resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} @@ -12842,6 +13043,46 @@ packages: resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} engines: {node: '>=18'} + '@smithy/core@3.31.0': + resolution: {integrity: sha512-sylYk2l9d7CmRv8ts8p0SDQUr3VO+HMeS1nrjL6+UtbO8ktJHTOeQ1McX+aAyvGGccp5aZX9eNtdcXrSwzoZaw==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.4.15': + resolution: {integrity: sha512-xYVGrisQqTJWhOnScUhbx8s9H63TMtoxzuUoxG6mP8J+B/YbX3vZxVsgV0xDf43abJnJP0fjP7BkQh7OESwuRA==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.6.12': + resolution: {integrity: sha512-OpQgP6IGH4j0NJ2zjfYZLjQL85ai+Wi/q51EmZJovXsEwKSvu89qiXUq77Q6EmwZ/hSl7fKpn2Z9mhiDN6OM+Q==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/node-http-handler@4.7.3': + resolution: {integrity: sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.9.12': + resolution: {integrity: sha512-dWW5KRt4mnEvjNzbGqGeCuAvgum85Y9ZoyuMQqcTEfapndyVJ1k9BEHK7kdXJZ32enyRmmwcFjMwlB/KgLKI3Q==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.6.11': + resolution: {integrity: sha512-7HsspeiNCZvZHEJ22vV5L/QYuJdTyJvPJvMrYD3AgkM3IJB0pkln4jkjPvtpTWRMkHXbO8WKwNjoVdVlBFwHmw==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.16.1': + resolution: {integrity: sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + '@speed-highlight/core@1.2.15': resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==} @@ -14712,6 +14953,9 @@ packages: botframework-schema@4.23.3: resolution: {integrity: sha512-/W0uWxZ3fuPLAImZRLnPTbs49Z2xMpJSIzIBxSfvwO0aqv9GsM3bTk3zlNdJ1xr40SshQ7WiH2H1hgjBALwYJw==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + bplist-creator@0.1.0: resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==} @@ -15590,6 +15834,10 @@ packages: diff-match-patch@1.0.5: resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + diff@9.0.0: resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} engines: {node: '>=0.3.1'} @@ -16750,6 +16998,9 @@ packages: hermes-parser@0.36.1: resolution: {integrity: sha512-GApNk4zLHi2UWoWZZkx7LNCOSzLSc5lB55pZ/PhK7ycFeg7u5LcF88p/WbpIi1XUDtE0MpHE3uRR3u3KB7TjSQ==} + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + highlight.js@11.11.1: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} @@ -16775,6 +17026,10 @@ packages: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} + hosted-git-info@9.0.3: + resolution: {integrity: sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==} + engines: {node: ^20.17.0 || >=22.9.0} + html-encoding-sniffer@6.0.0: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -16874,6 +17129,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + image-blob-reduce@3.0.1: resolution: {integrity: sha512-/VmmWgIryG/wcn4TVrV7cC4mlfUC/oyiKIfSg5eVM3Ten/c1c34RJhMYKCWTnoSMHSqXLt3tsrBR4Q2HInvN+Q==} @@ -18308,6 +18567,18 @@ packages: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} + openai@6.26.0: + resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + opencollective-postinstall@2.0.3: resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==} hasBin: true @@ -18452,6 +18723,9 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + partial-json@0.1.7: + resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -20080,6 +20354,9 @@ packages: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} + typebox@1.1.38: + resolution: {integrity: sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==} + typed-query-selector@2.12.2: resolution: {integrity: sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==} @@ -21200,6 +21477,220 @@ snapshots: '@types/react': 19.2.17 react: 19.2.7 + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.974.2 + '@aws-sdk/util-locate-window': 3.965.8 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.974.2 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.974.2 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-bedrock-runtime@3.1048.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.977.1 + '@aws-sdk/credential-provider-node': 3.972.73 + '@aws-sdk/eventstream-handler-node': 3.972.30 + '@aws-sdk/middleware-eventstream': 3.972.25 + '@aws-sdk/middleware-websocket': 3.972.44 + '@aws-sdk/token-providers': 3.1048.0 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/fetch-http-handler': 5.6.12 + '@smithy/node-http-handler': 4.7.3 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/core@3.977.1': + dependencies: + '@aws-sdk/types': 3.974.2 + '@aws-sdk/xml-builder': 3.972.37 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.31.0 + '@smithy/signature-v4': 5.6.11 + '@smithy/types': 4.16.1 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.62': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.64': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/fetch-http-handler': 5.6.12 + '@smithy/node-http-handler': 4.9.12 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.973.7': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/credential-provider-env': 3.972.62 + '@aws-sdk/credential-provider-http': 3.972.64 + '@aws-sdk/credential-provider-login': 3.972.69 + '@aws-sdk/credential-provider-process': 3.972.62 + '@aws-sdk/credential-provider-sso': 3.973.6 + '@aws-sdk/credential-provider-web-identity': 3.972.68 + '@aws-sdk/nested-clients': 3.997.36 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/credential-provider-imds': 4.4.15 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.69': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/nested-clients': 3.997.36 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.73': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.62 + '@aws-sdk/credential-provider-http': 3.972.64 + '@aws-sdk/credential-provider-ini': 3.973.7 + '@aws-sdk/credential-provider-process': 3.972.62 + '@aws-sdk/credential-provider-sso': 3.973.6 + '@aws-sdk/credential-provider-web-identity': 3.972.68 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/credential-provider-imds': 4.4.15 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.62': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.973.6': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/nested-clients': 3.997.36 + '@aws-sdk/token-providers': 3.1096.0 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.68': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/nested-clients': 3.997.36 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/eventstream-handler-node@3.972.30': + dependencies: + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-eventstream@3.972.25': + dependencies: + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-websocket@3.972.44': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/fetch-http-handler': 5.6.12 + '@smithy/signature-v4': 5.6.11 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.36': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/signature-v4-multi-region': 3.996.42 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/fetch-http-handler': 5.6.12 + '@smithy/node-http-handler': 4.9.12 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.42': + dependencies: + '@aws-sdk/types': 3.974.2 + '@smithy/signature-v4': 5.6.11 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1048.0': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/nested-clients': 3.997.36 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1096.0': + dependencies: + '@aws-sdk/core': 3.977.1 + '@aws-sdk/nested-clients': 3.997.36 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/types@3.974.2': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.8': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.37': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.3.0': {} + '@azure/abort-controller@2.1.2': dependencies: tslib: 2.8.1 @@ -22227,6 +22718,77 @@ snapshots: '@drizzle-team/brocli@0.10.2': {} + '@earendil-works/pi-agent-core@0.82.1(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.18.0)(zod@4.4.3)': + dependencies: + '@earendil-works/pi-ai': 0.82.1(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.18.0)(zod@4.4.3) + diff: 8.0.4 + ignore: 7.0.5 + typebox: 1.1.38 + yaml: 2.9.0 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-ai@0.82.1(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.18.0)(zod@4.4.3)': + dependencies: + '@anthropic-ai/sdk': 0.90.0(zod@4.4.3) + '@aws-sdk/client-bedrock-runtime': 3.1048.0 + '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3)) + '@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0) + '@opentelemetry/api': 1.9.0 + '@smithy/node-http-handler': 4.7.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + openai: 6.26.0(ws@8.18.0)(zod@4.4.3) + partial-json: 0.1.7 + typebox: 1.1.38 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-coding-agent@0.82.1(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.18.0)(zod@4.4.3)': + dependencies: + '@earendil-works/pi-agent-core': 0.82.1(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.18.0)(zod@4.4.3) + '@earendil-works/pi-ai': 0.82.1(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.18.0)(zod@4.4.3) + '@earendil-works/pi-tui': 0.82.1 + '@silvia-odwyer/photon-node': 0.3.4 + chalk: 5.6.2 + cross-spawn: 7.0.6 + diff: 8.0.4 + glob: 13.0.6 + highlight.js: 10.7.3 + hosted-git-info: 9.0.3 + ignore: 7.0.5 + jiti: 2.7.0 + minimatch: 10.2.5 + proper-lockfile: 4.1.2 + semver: 7.8.0 + typebox: 1.1.38 + undici: 7.28.0 + yaml: 2.9.0 + optionalDependencies: + '@mariozechner/clipboard': 0.3.9 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-tui@0.82.1': + dependencies: + get-east-asian-width: 1.6.0 + marked: 18.0.5 + '@electron-internal/extract-zip@1.0.4': {} '@electron/asar@3.4.1': @@ -24122,6 +24684,50 @@ snapshots: '@marijn/find-cluster-break@1.0.2': {} + '@mariozechner/clipboard-darwin-arm64@0.3.9': + optional: true + + '@mariozechner/clipboard-darwin-universal@0.3.9': + optional: true + + '@mariozechner/clipboard-darwin-x64@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + optional: true + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + optional: true + + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + optional: true + + '@mariozechner/clipboard@0.3.9': + optionalDependencies: + '@mariozechner/clipboard-darwin-arm64': 0.3.9 + '@mariozechner/clipboard-darwin-universal': 0.3.9 + '@mariozechner/clipboard-darwin-x64': 0.3.9 + '@mariozechner/clipboard-linux-arm64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-arm64-musl': 0.3.9 + '@mariozechner/clipboard-linux-riscv64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-musl': 0.3.9 + '@mariozechner/clipboard-win32-arm64-msvc': 0.3.9 + '@mariozechner/clipboard-win32-x64-msvc': 0.3.9 + optional: true + '@mediapipe/tasks-vision@0.10.17': {} '@mediapipe/tasks-vision@0.10.35': {} @@ -24136,6 +24742,18 @@ snapshots: dependencies: '@chevrotain/types': 11.1.2 + '@mistralai/mistralai@2.2.6(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/semantic-conventions': 1.41.1 + ws: 8.21.0 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + optionalDependencies: + '@opentelemetry/api': 1.9.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@mixmark-io/domino@2.2.0': {} '@modelcontextprotocol/client@2.0.0': @@ -24412,6 +25030,8 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.1 + '@opentelemetry/api@1.9.0': {} + '@opentelemetry/api@1.9.1': {} '@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1)': @@ -28513,12 +29133,67 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@silvia-odwyer/photon-node@0.3.4': {} + '@sinclair/typebox@0.27.10': {} '@sindresorhus/is@4.6.0': {} '@sindresorhus/is@7.2.0': {} + '@smithy/core@3.31.0': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.4.15': + dependencies: + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.6.12': + dependencies: + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/node-http-handler@4.7.3': + dependencies: + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.9.12': + dependencies: + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/signature-v4@5.6.11': + dependencies: + '@smithy/core': 3.31.0 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/types@4.16.1': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + '@speed-highlight/core@1.2.15': {} '@standard-schema/spec@1.1.0': {} @@ -30556,6 +31231,8 @@ snapshots: uuid: 10.0.0 zod: 3.25.76 + bowser@2.14.1: {} + bplist-creator@0.1.0: dependencies: stream-buffers: 2.2.0 @@ -31421,6 +32098,8 @@ snapshots: diff-match-patch@1.0.5: {} + diff@8.0.4: {} + diff@9.0.0: {} dingbat-to-unicode@1.0.1: {} @@ -32932,6 +33611,8 @@ snapshots: dependencies: hermes-estree: 0.36.1 + highlight.js@10.7.3: {} + highlight.js@11.11.1: {} hls.js@1.6.16: {} @@ -32952,6 +33633,10 @@ snapshots: dependencies: lru-cache: 10.4.3 + hosted-git-info@9.0.3: + dependencies: + lru-cache: 11.5.1 + html-encoding-sniffer@6.0.0(@noble/hashes@2.2.0): dependencies: '@exodus/bytes': 1.15.1(@noble/hashes@2.2.0) @@ -33053,6 +33738,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + image-blob-reduce@3.0.1: dependencies: pica: 7.1.1 @@ -34780,6 +35467,11 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + openai@6.26.0(ws@8.18.0)(zod@4.4.3): + optionalDependencies: + ws: 8.18.0 + zod: 4.4.3 + opencollective-postinstall@2.0.3: optional: true @@ -34954,6 +35646,8 @@ snapshots: parseurl@1.3.3: {} + partial-json@0.1.7: {} + path-browserify@1.0.1: {} path-data-parser@0.1.0: {} @@ -36919,6 +37613,8 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 + typebox@1.1.38: {} + typed-query-selector@2.12.2: {} typedarray@0.0.6: {} From 976a8104a4ae8742d56ed2c69d266a4bf850e2c3 Mon Sep 17 00:00:00 2001 From: RogierKonings Date: Wed, 29 Jul 2026 15:48:33 +0200 Subject: [PATCH 05/10] chore(core): prepare local Pi prerelease 0.130.0-local-pi.1 --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 27992c8a68..189dc91c44 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@agent-native/core", - "version": "0.129.1", + "version": "0.130.0-local-pi.1", "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context", "homepage": "https://github.com/BuilderIO/agent-native#readme", "bugs": { From 30e67db49966479991e5de2a6fc86d6f12f6c2d4 Mon Sep 17 00:00:00 2001 From: RogierKonings Date: Wed, 29 Jul 2026 15:50:45 +0200 Subject: [PATCH 06/10] Revert "chore(core): prepare local Pi prerelease 0.130.0-local-pi.1" This reverts commit 976a8104a4ae8742d56ed2c69d266a4bf850e2c3. --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 189dc91c44..27992c8a68 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@agent-native/core", - "version": "0.130.0-local-pi.1", + "version": "0.129.1", "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context", "homepage": "https://github.com/BuilderIO/agent-native#readme", "bugs": { From 890b6f138f336eaf2f4b2dafb25d1838caed3e88 Mon Sep 17 00:00:00 2001 From: RogierKonings Date: Wed, 29 Jul 2026 16:15:35 +0200 Subject: [PATCH 07/10] fix(core): settle harness startup activity --- packages/core/src/agent/harness/runner.spec.ts | 1 - packages/core/src/agent/harness/runner.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/core/src/agent/harness/runner.spec.ts b/packages/core/src/agent/harness/runner.spec.ts index a296c7db65..4bc522a987 100644 --- a/packages/core/src/agent/harness/runner.spec.ts +++ b/packages/core/src/agent/harness/runner.spec.ts @@ -92,7 +92,6 @@ describe("startAgentHarnessRun", () => { { type: "activity", label: "Starting Fake Harness", - tool: "harness", }, { type: "text", text: "Hello" }, { type: "tool_start", tool: "read", input: { path: "a.ts" } }, diff --git a/packages/core/src/agent/harness/runner.ts b/packages/core/src/agent/harness/runner.ts index 2ab1fb2477..b830e536cf 100644 --- a/packages/core/src/agent/harness/runner.ts +++ b/packages/core/src/agent/harness/runner.ts @@ -88,7 +88,6 @@ export function startAgentHarnessRun( send({ type: "activity", label: `Starting ${opts.adapter.label}`, - tool: "harness", }); harnessSession ??= await opts.adapter.createSession({ From 297a94f29fc1fe5a1be2452019c8c0f0886a12a1 Mon Sep 17 00:00:00 2001 From: RogierKonings Date: Wed, 29 Jul 2026 16:15:45 +0200 Subject: [PATCH 08/10] chore(core): prepare local Pi prerelease 0.130.0-local-pi.2 --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 27992c8a68..1cce64ad19 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@agent-native/core", - "version": "0.129.1", + "version": "0.130.0-local-pi.2", "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context", "homepage": "https://github.com/BuilderIO/agent-native#readme", "bugs": { From 22b345f2853324bc19dbc519947c9a644fc28536 Mon Sep 17 00:00:00 2001 From: RogierKonings Date: Wed, 29 Jul 2026 16:17:58 +0200 Subject: [PATCH 09/10] Revert "chore(core): prepare local Pi prerelease 0.130.0-local-pi.2" This reverts commit 297a94f29fc1fe5a1be2452019c8c0f0886a12a1. --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 1cce64ad19..27992c8a68 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@agent-native/core", - "version": "0.130.0-local-pi.2", + "version": "0.129.1", "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context", "homepage": "https://github.com/BuilderIO/agent-native#readme", "bugs": { From 580b72098c76640b9b877dea546ca16b126d0ac7 Mon Sep 17 00:00:00 2001 From: RogierKonings Date: Wed, 29 Jul 2026 16:31:49 +0200 Subject: [PATCH 10/10] test(core): freeze Neon statement budget clock --- packages/core/src/db/client.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/db/client.spec.ts b/packages/core/src/db/client.spec.ts index a30dadb71f..b76e4fff96 100644 --- a/packages/core/src/db/client.spec.ts +++ b/packages/core/src/db/client.spec.ts @@ -718,6 +718,8 @@ describe("Neon foreground statement budgets", () => { }); it("sets and resets a server-side timeout for an explicitly budgeted query", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-01-01T00:00:00Z")); vi.stubEnv("NETLIFY", "true"); const query = vi.fn(async (sql: string) => sql === "SELECT 1"