diff --git a/README.md b/README.md index 99ea22e..0882680 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Runtime data is stored under `.thoughtstream/`. Set `THOUGHTSTREAM_ROOT` to use | --- | --- | | `pnpm demo` | Run the fixture-backed filesystem demo. | | `pnpm scan -- --root --source filesystem:` | Scan a filesystem source once. | -| `pnpm watch -- --root --source filesystem:` | Watch a filesystem source until interrupted. Deployed split-process sources use `--producer-only`. | +| `pnpm watch -- --root --source filesystem:` | Watch a filesystem source until interrupted; add `--producer-only` when a separate consumer service owns declarations. | | `pnpm thought rss --url --source rss:` | Poll an RSS or Atom feed once. | | `pnpm thought jetstream --source jetstream: --collections ` | Run a bounded ATProto Jetstream subscription. | | `pnpm thought telegram-spool --file --source telegram:` | Ingest an append-only Telegram NDJSON spool. | @@ -223,6 +223,18 @@ Pi declarations may opt into the read-only `atproto.fetch-markdown` and `web.dow Output contracts are versioned registry entries rather than one universal observation shape. [`agents/conceptualizer.yaml`](agents/conceptualizer.yaml) defines the active OpenAI-backed consumer using `stream.thought.output.conceptualization@1`. A successful run settles one bounded private `stream.thought.derived.concept.graph` event with source/run lineage. It does not publish ATProto records or mutate an external graph. +[`agents/coil-public-knowledge.yaml`](agents/coil-public-knowledge.yaml) is the disabled stateful Coil recommendation consumer. One explicit producer watches the Coil as `filesystem:coil`; the trusted consumer resolves each immutable Jazz document version, applies the default-deny Public Knowledge policy, and skips blocked/deferred/deleted inputs before opening an SDK session. Each eligible stable `documentId` retains one conversation on the configured existing agent across renames and declaration upgrades. The local SDK profile uses an API-backed App Server, strict permission mode, no tools or skills, and memory-root filesystem confinement. Its only accepted effect is one sensitive `stream.thought.derived.public-knowledge.recommendation`; it cannot edit, stage, publish, write protocol records, or deploy. + +Activation is environment-bound rather than a tracked `enabled: true`. The consumer process requires `THOUGHTSTREAM_ENABLE_COIL_PUBLIC_KNOWLEDGE=1`, `THOUGHTSTREAM_LETTA_CO_AGENT_ID`, `THOUGHTSTREAM_LETTA_CO_MEMORY_DIR`, `THOUGHTSTREAM_PUBLIC_KNOWLEDGE_POLICY_PATH`, and `THOUGHTSTREAM_PUBLIC_KNOWLEDGE_CATALOG_ROOT`. The filesystem producer remains a separate process: + +```sh +pnpm thought watch --producer-only --root --source filesystem:coil +``` + +The declaration serializes one agent operation at a time and uses durable 30-second, hourly, and daily accounting windows. Enabling it against an existing source intentionally replays that declaration version from the beginning; run a bounded canary before full activation. + +Observed Agent SDK 0.5.7 behavior: exact `summarySearch` plus conversation management is sufficient to recover a remote conversation created before Jazz binding evidence settles, and local API-backed sessions successfully enforce the memory-root/no-tools profile. The local management transport keeps its SDK-owned App Server pooled and exposes no top-level client shutdown method, so one-shot probes do not naturally terminate after their result. Long-running production consumers therefore own the App Server in one systemd control group; bounded canaries require an external process-group supervisor. This is the same lifecycle gap tracked in [letta-agent-sdk#245](https://github.com/letta-ai/letta-agent-sdk/issues/245). + [`agents/review-candidate-a.example.yaml`](agents/review-candidate-a.example.yaml) and [`agents/review-candidate-b.example.yaml`](agents/review-candidate-b.example.yaml) show the disabled two-candidate path. Both consume the same `stream.thought.source.review.prompt`, emit strict `stream.thought.output.review-response@1` outputs, and have no tools or external actions. `review-item` freezes the exact completed pair before browser grading. `spec/review.md` owns judgeability, correction, blinding, supersession, browser authority, and export semantics. A local public-safe canary starts from the reviewed payload fixture: diff --git a/agents/coil-public-knowledge.yaml b/agents/coil-public-knowledge.yaml new file mode 100644 index 0000000..743af7b --- /dev/null +++ b/agents/coil-public-knowledge.yaml @@ -0,0 +1,69 @@ +id: coil-public-knowledge +version: 2 +name: Coil Public Knowledge recommendations +description: Keep one Co conversation per eligible Coil document and propose private Public Knowledge revisions or additions. +enabled: false +enabledEnv: THOUGHTSTREAM_ENABLE_COIL_PUBLIC_KNOWLEDGE +outputContract: + id: stream.thought.output.public-knowledge-recommendation + version: 2 +subscribe: + types: + - stream.thought.source.file.added + - stream.thought.source.file.changed + - stream.thought.source.file.renamed + - stream.thought.source.file.deleted + sources: + - filesystem:coil + privacy: + - sensitive + replay: beginning +context: + maxEvents: 1 + maxChars: 100000 + strategy: coil-public-knowledge + payloadFields: + - documentId + - path + - versionId + - sha256 +runner: + kind: letta-agent-sdk + backend: local + agentIdEnv: THOUGHTSTREAM_LETTA_CO_AGENT_ID + memoryDirEnv: THOUGHTSTREAM_LETTA_CO_MEMORY_DIR + conversation: per-document + responseMode: strict-json + permissionMode: strict + skillSources: [] + dreaming: + trigger: off + model: chatgpt-plus-pro/gpt-5.6-luna + reasoningEffort: low + maxOutputTokens: 2000 + timeoutMs: 180000 +accounting: + leaseMs: 240000 + reservation: + inputTokens: 60000 + outputTokens: 2000 + limits: + - window: rolling + durationMs: 30000 + maxCalls: 1 + maxInputTokens: 60000 + maxOutputTokens: 2000 + - window: hour + maxCalls: 20 + maxInputTokens: 1200000 + maxOutputTokens: 40000 + - window: day + maxCalls: 100 + maxInputTokens: 6000000 + maxOutputTokens: 200000 +prompt: prompts/coil-public-knowledge.md +emit: + - stream.thought.derived.public-knowledge.recommendation +policy: + tools: [] + externalActions: false diff --git a/package.json b/package.json index d6937e6..4a7b592 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@earendil-works/pi-agent-core": "^0.80.6", "@earendil-works/pi-ai": "^0.80.6", "@earendil-works/pi-coding-agent": "0.80.6", - "@letta-ai/letta-agent-sdk": "0.2.6", + "@letta-ai/letta-agent-sdk": "0.5.7", "chokidar": "^4.0.3", "diff": "^8.0.2", "fast-glob": "^3.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1385de2..91fa2b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,16 +20,16 @@ importers: version: 0.4.9 '@earendil-works/pi-agent-core': specifier: ^0.80.6 - version: 0.80.6(ws@8.21.0)(zod@4.4.3) + version: 0.80.6(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) '@earendil-works/pi-ai': specifier: ^0.80.6 - version: 0.80.6(ws@8.21.0)(zod@4.4.3) + version: 0.80.6(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) '@earendil-works/pi-coding-agent': specifier: 0.80.6 - version: 0.80.6(ws@8.21.0)(zod@4.4.3) + version: 0.80.6(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) '@letta-ai/letta-agent-sdk': - specifier: 0.2.6 - version: 0.2.6(ink@7.1.1(react@18.2.0))(react-dom@19.2.8(react@18.2.0))(zod@4.4.3) + specifier: 0.5.7 + version: 0.5.7(ink@7.1.1(react@18.2.0))(react-dom@19.2.8(react@18.2.0))(zod@4.4.3) chokidar: specifier: ^4.0.3 version: 4.0.3 @@ -291,13 +291,13 @@ packages: resolution: {integrity: sha512-Lvn89ko42h5ETUb6Z0Ku6ldskEqXaTdQBYvSa0+7bdG9V6rUEpXptv5e0OVZ1HDcvi8s6/2lGCQWsxKX+DFHNw==} engines: {node: '>=22.19.0'} - '@earendil-works/pi-ai@0.79.10': - resolution: {integrity: sha512-9jR23tOl0BIUdQMn70Gr72xYBpM7Xgl9Lyv7gAnU1USfkNRuYG/f/edLl+n/Dp/RafDW3JI4DF7y/GhgkORuew==} + '@earendil-works/pi-ai@0.80.6': + resolution: {integrity: sha512-7xfLk8sANBp+bpPEbjoOZTbPxsa+++b1JXAoSJsNa3vbs9AHHEclmvg54XLQcxH+fuwaeti/g2jeIfJ+mVYLpA==} engines: {node: '>=22.19.0'} hasBin: true - '@earendil-works/pi-ai@0.80.6': - resolution: {integrity: sha512-7xfLk8sANBp+bpPEbjoOZTbPxsa+++b1JXAoSJsNa3vbs9AHHEclmvg54XLQcxH+fuwaeti/g2jeIfJ+mVYLpA==} + '@earendil-works/pi-ai@0.82.1': + resolution: {integrity: sha512-3WFYRhEp3lQB3444EhPMBcM7zSaEUE3eJgHOR7s4081NLqbw/FsWilIKWXSua0Gv3sRr7m9xMidR3pPDE7jI/A==} engines: {node: '>=22.19.0'} hasBin: true @@ -948,6 +948,12 @@ packages: '@modelcontextprotocol/sdk': optional: true + '@hono/node-server@2.0.12': + resolution: {integrity: sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==} + engines: {node: '>=20'} + peerDependencies: + hono: ^4 + '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -1088,17 +1094,22 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@letta-ai/letta-agent-sdk@0.2.6': - resolution: {integrity: sha512-EKyjs0xL0YJF55n/Kszg7kaqIHUwIQXbj8hUbo2ytOIP7eA5NzFW6LtzF5lVUKVbVLWq5i+C+QYDtxA4gF9xhg==} + '@letta-ai/letta-agent-sdk@0.5.7': + resolution: {integrity: sha512-MQrc84S+i2Lxj/TccrRs+tVqbAA6FoRoHYmc06dSgapsSkuH2NgNOb9s+oBQ/yIOkO0Tpg63FlKc4Lo8lPo62Q==} + engines: {node: '>=22.19.0'} '@letta-ai/letta-client@1.12.1': resolution: {integrity: sha512-rYjXMXpkfssj7VBBX3qCp6mdpNRv6YPNrliYsjkhWoQDqGg3J9bsgIQ28ZhQTddabYxRUIwcdzuaizFx8pvZ7A==} - '@letta-ai/letta-code@0.27.30': - resolution: {integrity: sha512-HaSKBZgDbVn45nKBZtfIrOiYF10Sny9VXoOPM5IMOrdi33XldZy1NUTODt5YPO6AgTYGDI+nPAY3itPqFKrTuw==} + '@letta-ai/letta-code@0.29.12': + resolution: {integrity: sha512-ZG+N2MyVD4MjXKyjGJKm89Hjiu1tqHhrmYKhIwGBW4xvaW57hokimM7fI7p66fpWoFMnBfJHXNmAYxKVVqErUw==} engines: {node: '>=22.19.0'} hasBin: true + '@letta-ai/trajectory@0.2.0': + resolution: {integrity: sha512-biCyT0z8nh4Q8kIqBrPgmzoalacPmosmCpvEjdN9ILpL85+T75b8ahcN9MHPHQUVRj/J7UyQuRahJ4/JdrpCcA==} + engines: {node: '>=20'} + '@mariozechner/clipboard-darwin-arm64@0.3.9': resolution: {integrity: sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==} engines: {node: '>= 10'} @@ -1170,6 +1181,16 @@ packages: '@opentelemetry/api': optional: true + '@modelcontextprotocol/sdk@1.30.0': + resolution: {integrity: sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@noble/hashes@2.2.0': resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==} engines: {node: '>= 20.19.0'} @@ -1671,10 +1692,25 @@ packages: '@vscode/ripgrep@1.18.0': resolution: {integrity: sha512-ns5lWe44tSfbTMbVUsyB+I1819PVSw4AdpgK0RNkzfWfwy6+3IUNSxwSrfTno1/oWaS/hERNz+XLWVyga2aJBQ==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ansi-escapes@7.3.0: resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} @@ -1708,6 +1744,10 @@ packages: bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + bowser@2.14.1: resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} @@ -1726,10 +1766,22 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1774,13 +1826,41 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + convert-to-spaces@2.0.1: resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + core-js@3.49.0: resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cron-parser@5.6.2: + resolution: {integrity: sha512-yJ/G1LVir6CnlkLI40CsampPLKl1SprGGlUagWtGJxewytRVYezv5xVyzzbT+Pvzx+VIRvZVF7/a0eEMTxdnTA==} + engines: {node: '>=18'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1814,6 +1894,10 @@ packages: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1833,16 +1917,39 @@ packages: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + es-toolkit@1.50.0: resolution: {integrity: sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==} @@ -1866,6 +1973,9 @@ packages: engines: {node: '>=18'} hasBin: true + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} @@ -1873,17 +1983,45 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + expect-type@1.4.0: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} + express-rate-limit@8.6.1: + resolution: {integrity: sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-uri@3.1.5: + resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + fast-xml-builder@1.3.0: resolution: {integrity: sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==} @@ -1902,15 +2040,30 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gaxios@7.2.0: resolution: {integrity: sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg==} engines: {node: '>=18'} @@ -1923,6 +2076,14 @@ packages: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1939,6 +2100,10 @@ packages: resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} engines: {node: '>=14'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -1946,6 +2111,14 @@ packages: resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==} engines: {node: '>=12'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + hast-util-to-html@9.0.5: resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} @@ -1955,6 +2128,10 @@ packages: highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + hono@4.12.33: + resolution: {integrity: sha512-+SwvkaiJtxsiPjhy9LivY/1m7UsNqCJetM1BrZl9A5DkQhlbHQDU730mMiDPWjnoCYOM8Chf3WrCJw27kNTPFQ==} + engines: {node: '>=16.9.0'} + hosted-git-info@9.0.3: resolution: {integrity: sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==} engines: {node: ^20.17.0 || >=22.9.0} @@ -1962,6 +2139,10 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1970,6 +2151,10 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + ignore@7.0.5: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} @@ -1978,6 +2163,9 @@ packages: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ink-link@5.0.0: resolution: {integrity: sha512-TFDXc/0mwUW7LMjsr0/LeLxPVV5BnHDuDQff9RCgP4rb3R+V/4dIwGBZbCevcJZtQnVcW+Iz1LUrUbpq+UDwYA==} engines: {node: '>=18'} @@ -1997,6 +2185,14 @@ packages: react-devtools-core: optional: true + ip-address@10.4.0: + resolution: {integrity: sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + ipaddr.js@2.4.0: resolution: {integrity: sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==} engines: {node: '>= 10'} @@ -2032,6 +2228,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-unsafe@2.0.0: resolution: {integrity: sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==} @@ -2108,6 +2307,12 @@ packages: resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} engines: {node: '>=16'} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} @@ -2134,6 +2339,10 @@ packages: lru_map@0.4.1: resolution: {integrity: sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg==} + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -2142,9 +2351,21 @@ packages: engines: {node: '>= 20'} hasBin: true + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mdast-util-to-hast@13.2.1: resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2168,6 +2389,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -2191,6 +2420,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} @@ -2206,6 +2439,21 @@ packages: node-pty@1.1.0: resolution: {integrity: sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -2236,6 +2484,10 @@ packages: resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} engines: {node: '>=8'} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + partial-json@0.1.7: resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} @@ -2255,6 +2507,9 @@ packages: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} @@ -2269,6 +2524,10 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pluralize-esm@9.0.5: resolution: {integrity: sha512-Kb2dcpMsIutFw2hYrN0EhsAXOUJTd6FVMIxvNAkZCMQLVt9NGZqQczvGpYDxNWCZeCWLHUPxQIBudWzt1h7VVA==} engines: {node: '>=14.0.0'} @@ -2291,9 +2550,25 @@ packages: resolution: {integrity: sha512-oXf2UgIty8jnwfN4yvL1x79VLhL5uiKjZJbSGXGCIUmHmItTP4eS/UIlWDCeNx3seg+ujfn9vDlPMSrsh7wO+Q==} engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-dom@19.2.8: resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} peerDependencies: @@ -2322,6 +2597,10 @@ packages: regex@6.1.0: resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + restore-cursor@4.0.0: resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2343,6 +2622,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-applescript@7.1.0: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} @@ -2353,6 +2636,9 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -2361,6 +2647,17 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -2380,6 +2677,22 @@ packages: resolution: {integrity: sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==} engines: {node: '>=20'} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -2404,6 +2717,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -2463,6 +2780,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -2481,6 +2802,10 @@ packages: resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} engines: {node: '>=20'} + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + typebox@1.1.38: resolution: {integrity: sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==} @@ -2526,6 +2851,14 @@ packages: unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -2619,6 +2952,9 @@ packages: resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} engines: {node: '>=20'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.21.0: resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} @@ -3040,9 +3376,9 @@ snapshots: '@babel/runtime@7.29.7': {} - '@earendil-works/pi-agent-core@0.80.6(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-agent-core@0.80.6(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': dependencies: - '@earendil-works/pi-ai': 0.80.6(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-ai': 0.80.6(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) ignore: 7.0.5 typebox: 1.1.38 yaml: 2.9.0 @@ -3054,11 +3390,11 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.79.10(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-ai@0.80.6(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1048.0 - '@google/genai': 1.52.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 @@ -3075,17 +3411,17 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.80.6(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-ai@0.82.1(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.1)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1048.0 - '@google/genai': 1.52.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.21.0)(zod@4.4.3) + openai: 6.26.0(ws@8.21.1)(zod@4.4.3) partial-json: 0.1.7 typebox: 1.1.38 transitivePeerDependencies: @@ -3096,10 +3432,10 @@ snapshots: - ws - zod - '@earendil-works/pi-coding-agent@0.80.6(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-coding-agent@0.80.6(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': dependencies: - '@earendil-works/pi-agent-core': 0.80.6(ws@8.21.0)(zod@4.4.3) - '@earendil-works/pi-ai': 0.80.6(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-agent-core': 0.80.6(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-ai': 0.80.6(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) '@earendil-works/pi-tui': 0.80.7 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 @@ -3451,17 +3787,23 @@ snapshots: '@garden-co/jazz-napi-win32-x64-msvc@2.0.0-alpha.53': optional: true - '@google/genai@1.52.0': + '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))': dependencies: google-auth-library: 10.9.0 p-retry: 4.6.2 protobufjs: 7.6.5 ws: 8.21.0 + optionalDependencies: + '@modelcontextprotocol/sdk': 1.30.0(zod@4.4.3) transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate + '@hono/node-server@2.0.12(hono@4.12.33)': + dependencies: + hono: 4.12.33 + '@img/colour@1.1.0': {} '@img/sharp-darwin-arm64@0.34.5': @@ -3560,11 +3902,12 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} - '@letta-ai/letta-agent-sdk@0.2.6(ink@7.1.1(react@18.2.0))(react-dom@19.2.8(react@18.2.0))(zod@4.4.3)': + '@letta-ai/letta-agent-sdk@0.5.7(ink@7.1.1(react@18.2.0))(react-dom@19.2.8(react@18.2.0))(zod@4.4.3)': dependencies: - '@letta-ai/letta-code': 0.27.30(ink@7.1.1(react@18.2.0))(react-dom@19.2.8(react@18.2.0))(zod@4.4.3) + '@letta-ai/letta-client': 1.12.1 + '@letta-ai/letta-code': 0.29.12(ink@7.1.1(react@18.2.0))(react-dom@19.2.8(react@18.2.0))(zod@4.4.3) transitivePeerDependencies: - - '@modelcontextprotocol/sdk' + - '@cfworker/json-schema' - bufferutil - ink - react-dom @@ -3574,12 +3917,15 @@ snapshots: '@letta-ai/letta-client@1.12.1': {} - '@letta-ai/letta-code@0.27.30(ink@7.1.1(react@18.2.0))(react-dom@19.2.8(react@18.2.0))(zod@4.4.3)': + '@letta-ai/letta-code@0.29.12(ink@7.1.1(react@18.2.0))(react-dom@19.2.8(react@18.2.0))(zod@4.4.3)': dependencies: - '@earendil-works/pi-ai': 0.79.10(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-ai': 0.82.1(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.1)(zod@4.4.3) '@letta-ai/letta-client': 1.12.1 + '@letta-ai/trajectory': 0.2.0 + '@modelcontextprotocol/sdk': 1.30.0(zod@4.4.3) '@pierre/diffs': 1.2.2(react-dom@19.2.8(react@18.2.0))(react@18.2.0) '@scarf/scarf': 1.4.0 + cron-parser: 5.6.2 glob: 13.0.6 ink-link: 5.0.0(ink@7.1.1(react@18.2.0)) node-pty: 1.1.0 @@ -3588,11 +3934,11 @@ snapshots: sharp: 0.34.5 shiki: 4.3.1 strip-ansi: 7.2.0 - ws: 8.21.0 + ws: 8.21.1 optionalDependencies: '@vscode/ripgrep': 1.18.0 transitivePeerDependencies: - - '@modelcontextprotocol/sdk' + - '@cfworker/json-schema' - bufferutil - ink - react-dom @@ -3600,6 +3946,8 @@ snapshots: - utf-8-validate - zod + '@letta-ai/trajectory@0.2.0': {} + '@mariozechner/clipboard-darwin-arm64@0.3.9': optional: true @@ -3656,6 +4004,28 @@ snapshots: - bufferutil - utf-8-validate + '@modelcontextprotocol/sdk@1.30.0(zod@4.4.3)': + dependencies: + '@hono/node-server': 2.0.12(hono@4.12.33) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.1.0 + express: 5.2.1 + express-rate-limit: 8.6.1(express@5.2.1) + hono: 4.12.33 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + '@noble/hashes@2.2.0': {} '@nodable/entities@3.0.0': {} @@ -4143,8 +4513,24 @@ snapshots: '@vscode/ripgrep-win32-x64': 1.18.0 optional: true + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + agent-base@7.1.4: {} + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.5 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-escapes@7.3.0: dependencies: environment: 1.1.0 @@ -4165,6 +4551,20 @@ snapshots: bignumber.js@9.3.1: {} + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.0.0 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + on-finished: 2.4.1 + qs: 6.15.3 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + bowser@2.14.1: {} brace-expansion@5.0.8: @@ -4181,8 +4581,20 @@ snapshots: dependencies: run-applescript: 7.1.0 + bytes@3.1.2: {} + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + ccount@2.0.1: {} chai@5.3.3: @@ -4222,10 +4634,29 @@ snapshots: comma-separated-tokens@2.0.3: {} + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + convert-to-spaces@2.0.1: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + core-js@3.49.0: {} + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cron-parser@5.6.2: + dependencies: + luxon: 3.7.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -4249,6 +4680,8 @@ snapshots: define-lazy-prop@3.0.0: {} + depd@2.0.0: {} + dequal@2.0.3: {} detect-libc@2.1.2: {} @@ -4261,14 +4694,32 @@ snapshots: diff@8.0.4: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + environment@1.1.0: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + es-toolkit@1.50.0: {} esbuild@0.21.5: @@ -4384,16 +4835,69 @@ snapshots: '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 + escape-html@1.0.3: {} + escape-string-regexp@2.0.0: {} estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 + etag@1.8.1: {} + + eventsource-parser@3.1.0: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.1.0 + expect-type@1.4.0: {} + express-rate-limit@8.6.1(express@5.2.1): + dependencies: + debug: 4.4.3 + express: 5.2.1 + ip-address: 10.4.0 + transitivePeerDependencies: + - supports-color + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.3 + range-parser: 1.3.0 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + extend@3.0.2: {} + fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4402,6 +4906,8 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-uri@3.1.5: {} + fast-xml-builder@1.3.0: dependencies: path-expression-matcher: 1.6.2 @@ -4429,13 +4935,30 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 + forwarded@0.2.0: {} + + fresh@2.0.0: {} + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + gaxios@7.2.0: dependencies: extend: 3.0.2 @@ -4454,6 +4977,24 @@ snapshots: get-east-asian-width@1.6.0: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -4477,10 +5018,18 @@ snapshots: google-logging-utils@1.1.3: {} + gopd@1.2.0: {} + graceful-fs@4.2.11: {} has-flag@5.0.1: {} + has-symbols@1.1.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + hast-util-to-html@9.0.5: dependencies: '@types/hast': 3.0.5 @@ -4501,12 +5050,22 @@ snapshots: highlight.js@10.7.3: {} + hono@4.12.33: {} + hosted-git-info@9.0.3: dependencies: lru-cache: 11.5.2 html-void-elements@3.0.0: {} + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -4521,10 +5080,16 @@ snapshots: transitivePeerDependencies: - supports-color + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + ignore@7.0.5: {} indent-string@5.0.0: {} + inherits@2.0.4: {} + ink-link@5.0.0(ink@7.1.1(react@18.2.0)): dependencies: ink: 7.1.1(react@18.2.0) @@ -4562,6 +5127,10 @@ snapshots: - bufferutil - utf-8-validate + ip-address@10.4.0: {} + + ipaddr.js@1.9.1: {} + ipaddr.js@2.4.0: {} is-docker@3.0.0: {} @@ -4584,6 +5153,8 @@ snapshots: is-number@7.0.0: {} + is-promise@4.0.0: {} + is-unsafe@2.0.0: {} is-wsl@3.1.1: @@ -4640,6 +5211,10 @@ snapshots: '@babel/runtime': 7.29.7 ts-algebra: 2.0.0 + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 @@ -4665,12 +5240,16 @@ snapshots: lru_map@0.4.1: {} + luxon@3.7.2: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 marked@18.0.5: {} + math-intrinsics@1.1.0: {} + mdast-util-to-hast@13.2.1: dependencies: '@types/hast': 3.0.5 @@ -4683,6 +5262,10 @@ snapshots: unist-util-visit: 5.1.0 vfile: 6.0.3 + media-typer@1.1.1: {} + + merge-descriptors@2.0.0: {} + merge2@1.4.1: {} micromark-util-character@2.1.1: @@ -4707,6 +5290,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mimic-fn@2.1.0: {} minimatch@10.2.5: @@ -4721,6 +5310,8 @@ snapshots: nanoid@3.3.16: {} + negotiator@1.0.0: {} + node-addon-api@7.1.1: {} node-domexception@1.0.0: {} @@ -4735,6 +5326,18 @@ snapshots: dependencies: node-addon-api: 7.1.1 + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -4759,11 +5362,18 @@ snapshots: ws: 8.21.0 zod: 4.4.3 + openai@6.26.0(ws@8.21.1)(zod@4.4.3): + optionalDependencies: + ws: 8.21.1 + zod: 4.4.3 + p-retry@4.6.2: dependencies: '@types/retry': 0.12.0 retry: 0.13.1 + parseurl@1.3.3: {} + partial-json@0.1.7: {} patch-console@2.0.0: {} @@ -4777,6 +5387,8 @@ snapshots: lru-cache: 11.5.2 minipass: 7.1.3 + path-to-regexp@8.4.2: {} + pathe@1.1.2: {} pathval@2.0.1: {} @@ -4785,6 +5397,8 @@ snapshots: picomatch@2.3.2: {} + pkce-challenge@5.0.1: {} + pluralize-esm@9.0.5: {} postcss@8.5.19: @@ -4819,8 +5433,27 @@ snapshots: dependencies: long: 5.3.2 + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + queue-microtask@1.2.3: {} + range-parser@1.3.0: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + unpipe: 1.0.0 + react-dom@19.2.8(react@18.2.0): dependencies: react: 18.2.0 @@ -4847,6 +5480,8 @@ snapshots: dependencies: regex-utilities: 2.3.0 + require-from-string@2.0.2: {} + restore-cursor@4.0.0: dependencies: onetime: 5.1.2 @@ -4889,6 +5524,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.62.2 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + run-applescript@7.1.0: {} run-parallel@1.2.0: @@ -4897,10 +5542,39 @@ snapshots: safe-buffer@5.2.1: {} + safer-buffer@2.1.2: {} + scheduler@0.27.0: {} semver@7.8.0: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.3.0 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + sharp@0.34.5: dependencies: '@img/colour': 1.1.0 @@ -4960,6 +5634,34 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@3.0.7: {} @@ -4979,6 +5681,8 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} string-width@8.2.2: @@ -5029,6 +5733,8 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + trim-lines@3.0.1: {} ts-algebra@2.0.0: {} @@ -5045,6 +5751,12 @@ snapshots: dependencies: tagged-tag: 1.0.0 + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.1 + mime-types: 3.0.2 + typebox@1.1.38: {} typescript@5.9.3: {} @@ -5084,6 +5796,10 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 + unpipe@1.0.0: {} + + vary@1.1.2: {} + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -5179,6 +5895,8 @@ snapshots: string-width: 8.2.2 strip-ansi: 7.2.0 + wrappy@1.0.2: {} + ws@8.21.0: {} ws@8.21.1: {} diff --git a/prompts/coil-public-knowledge.md b/prompts/coil-public-knowledge.md new file mode 100644 index 0000000..b17a625 --- /dev/null +++ b/prompts/coil-public-knowledge.md @@ -0,0 +1,7 @@ +You are Co operating in one durable conversation bound to one stable Coil document. + +Evaluate only whether the current exact document version contains material that should revise existing Public Knowledge entries, become one new entry, or be skipped. The current packet includes private source evidence and public catalog metadata. Your own memory may inform editorial judgment but is not publication evidence and must not leak private context into the recommendation. + +Prefer revising existing entries over creating overlapping pages. Name only catalog slugs present in the packet when revising. A new candidate should have a durable concept-level scope, not merely summarize the source note. Identify claims that need primary-source verification. Treat blocked wikilinks, personal material, private names, health, relationships, internal operations, unpublished plans, and work details as privacy findings. + +This is a recommendation-only conversation. You cannot edit The Coil, stage content, publish, write ATProto records, deploy, or authorize later effects. diff --git a/spec/agents.md b/spec/agents.md index b84f550..6292c57 100644 --- a/spec/agents.md +++ b/spec/agents.md @@ -99,11 +99,11 @@ Provider JSON response formatting is a capability claim, not a request decoratio Tool-using or persistent agent runtimes use the separate generic contract in `harnesses.md`. The first reference adapter is `pi-coding@1` in the `workspace-v1` container profile. Declarations select only an allowlisted adapter/profile and trusted model tier. They cannot supply an image, host mount, executable extension, provider URL, credential reference, broker budget, or container flag. Any container, broker, or lease setup failure leaves source progress unchanged for a later retry. Neither profile has an in-process or trusted-host fallback. -The `letta-agent-sdk` runner is a distinct stateful harness adapter. Its first supported profile is `letta-cloud-v1`: the trusted consumer uses the Letta Agent SDK Cloud backend, while Letta supplies the managed sandbox and persistent agent/conversation state. Jazz remains authoritative for source events, consumer progress, lifecycle evidence, accepted outputs, and channel delivery. The Letta agent owns its conversational continuity and agent memory. ThoughtStream must not rebuild a synthetic transcript and send it again on every turn. +The `letta-agent-sdk` runner is a distinct stateful harness adapter. The `letta-cloud-v1` profile uses the Agent SDK Cloud backend and a managed sandbox. The `letta-local-memory-v1` profile uses an SDK-owned loopback App Server against an existing cloud-backed agent, while tools and skills are empty and the harness filesystem is fail-closed to that agent's memory root. Jazz remains authoritative for source events, consumer progress, lifecycle evidence, accepted outputs, and channel delivery. The Letta agent owns its conversational continuity and agent memory. ThoughtStream must not rebuild a synthetic transcript and send it again on every turn. -An enabled `letta-agent-sdk` declaration names a trusted Cloud agent through an environment-variable reference, uses a bounded set of concrete source namespaces, and selects the agent's main conversation. The declaration may choose Cloud sandbox TTL, permission mode, skill sources, dreaming trigger, and final-response mode. It cannot select an API-key variable, API base URL, websocket URL, sandbox image, host path, or remote environment. The trusted runtime always reads the ordinary `LETTA_API_KEY` credential and uses an SDK-managed Cloud sandbox. +An enabled `letta-agent-sdk` declaration names a trusted existing agent through an environment-variable reference and uses a bounded set of concrete source namespaces. It selects either the agent's main conversation or one conversation per stable filesystem document. The declaration may choose a fixed model handle, reasoning effort, permission mode, dreaming trigger, and final-response mode. Cloud declarations may choose sandbox TTL. Local-memory declarations name only the environment variable containing the agent memory root; they cannot select a general host path. No declaration can select an API-key variable, API base URL, websocket URL, sandbox image, arbitrary host root, or remote environment. -The initial stateful conversation topology is intentionally narrow: +The stateful conversation topologies are intentionally narrow: - one enabled declaration owns one Letta agent main conversation; - one or more explicitly named concrete ThoughtStream sources may feed that declaration; @@ -111,6 +111,11 @@ The initial stateful conversation topology is intentionally narrow: - the context strategy is `single-event` and the prompt contains only the current event packet plus trusted declaration instructions and a deterministic turn marker; - earlier Letta messages are available to the agent through its own conversation, but earlier ThoughtStream messages are not copied into the new user turn; - two enabled declarations may not share one Letta agent id, because concurrent sessions can race conversation state and MemFS. +- a `per-document` declaration accepts only filesystem add/change/rename events from one concrete source and uses `documentId` as the conversation scope key; +- Jazz and the remote conversation summary marker jointly recover the mapping, while the current path may change without changing conversation identity; +- all document conversations for one agent still share one scheduler operation key, so the default topology permits one Co turn at a time even though conversations are distinct. + +The Coil Public Knowledge context strategy runs its default-deny policy before SDK session creation. It loads the exact event-bound document version from Jazz, verifies source/document/path/version/hash consistency, and reads only frontmatter from the public catalog. Blocked, deferred, deleted, unsupported, oversized, stale, or policy-ambiguous inputs never open a conversation. The model sees the source document and public catalog as evidence but receives no vault browser or publication tool. Its output must satisfy `stream.thought.output.public-knowledge-recommendation@1`, including existing-target checks and candidate-slug collision checks against the exact catalog snapshot. Multi-source serialization is an execution-safety contract, not a globally canonical event order. Each source retains its own monotone sequence and independent consumer progress. When events from different sources become ready together, the resident sees them in the order their source operations enter the shared scheduler chain. That order is durable through each accepted turn and sufficient for one conversation owner; it must not be represented as a total order across the original source clocks. diff --git a/spec/architecture.md b/spec/architecture.md index 8e563d3..67c8031 100644 --- a/spec/architecture.md +++ b/spec/architecture.md @@ -42,6 +42,10 @@ For model-backed consumers, the runner builds a bounded context packet, invokes A conceptualizer consumer is a model-backed consumer that extracts concepts and directional links from one bounded source event. For an ATProto batch trigger, the trusted parent resolves every exact member reference and compiles the same bounded ATProto member views used by the resident path before inference; a list of opaque member ids is not sufficient conceptual evidence. It uses the `stream.thought.output.conceptualization@1` output contract, which validates one complete bounded graph of lowercase concept phrases and typed links. The runner settles that graph atomically as one `stream.thought.derived.concept.graph` event with exact source/run lineage. The conceptualizer has no outbound PDS authority; it produces only a private derived observation. Invalid graphs fail as a whole and emit no derived graph, so a partially parsed model answer cannot become durable state. Tinker is the inference provider; the conceptualizer does not train, sync an external graph, or publish to a PDS. +The Coil Public Knowledge consumer is a separate stateful Agent SDK declaration. It subscribes only to one concrete sensitive Coil filesystem source. Before opening an SDK session, the trusted parent resolves the exact immutable document version and applies the current default-deny Public Knowledge policy. Blocked, unmapped, unsupported, deleted, or oversized documents append a content-dark skipped-run receipt and advance that consumer's source progress without conversation creation or inference accounting. Eligible versions receive a bounded packet containing the exact source version plus public catalog frontmatter only. + +Each eligible stable `documentId` maps to one explicit conversation on Co's existing agent. Jazz stores that operational mapping; path is mutable metadata, so a confident rename preserves the conversation. A deterministic digest is written to the conversation summary, allowing a crash after remote conversation creation but before Jazz settlement to recover the same conversation instead of creating another. The declaration serializes all file conversations through one Co operation key while Jazz inference accounting applies the configured rolling/hour/day limits. The validated output is a private `stream.thought.derived.public-knowledge.recommendation` event. This consumer has no tools, external actions, Public Knowledge staging capability, file writer, ATProto writer, or deployment path. + ### 6. Projections Projectors are consumers with no special delivery path. They follow Jazz subscriptions, recover from per-source progress, and maintain rebuildable Jazz views: root activity, topic index, unresolved recommendations, source health, consumer health, document identity, and trace summaries. Jazz performs filtering, ordering, and bounded pagination. diff --git a/spec/events.md b/spec/events.md index 9590a5a..de5161b 100644 --- a/spec/events.md +++ b/spec/events.md @@ -97,6 +97,7 @@ An operational incident is a deterministic, content-dark projection over connect - `stream.thought.consumer.execution.completed` - `stream.thought.consumer.execution.failed` - `stream.thought.consumer.execution.blocked` +- `stream.thought.consumer.execution.skipped` - `stream.thought.consumer.execution.status-unknown` - `stream.thought.consumer.execution.abandoned` @@ -129,9 +130,12 @@ Proposal events are sensitive, append-only agent requests bound by the trusted p - `stream.thought.derived.retrieval.recommendation` - `stream.thought.derived.document.structure` - `stream.thought.derived.charter.reevaluation` +- `stream.thought.derived.public-knowledge.recommendation` Derived events are proposals or observations. A post candidate is never a publishing command. An output correction proposal is never an effective replacement without an active `accept` or `correct` judgment. +A Public Knowledge recommendation names one exact Coil document version and one contract-valid editorial disposition. It remains private even when every cited source is public. It cannot serve as a review receipt, promotion command, publication authorization, ATProto write request, or deployment trigger. + ### Judgments - `stream.thought.judgment.training-example` diff --git a/spec/jazz.md b/spec/jazz.md index b6ecd87..1be7a6a 100644 --- a/spec/jazz.md +++ b/spec/jazz.md @@ -78,6 +78,12 @@ Historical rows are caller-id inserts. The deployed Jazz permission policy must Operational rows are mutable. Adapter release identity and deployment catalog identity are content-addressed outside Jazz, then copied into consumer/run evidence for historical inspection. Activation and retirement require a new catalog generation plus coordinated restart; they are not database transitions. Accounting JSON readers accept both existing cost-bearing rows and new rows with omitted cost. Settlement follows the estimate captured on each reservation, so an active legacy reservation remains cost-tracked even after a declaration adopts a token-only policy. +### Stateful conversation bindings + +A file-scoped Letta conversation binding is keyed by declaration id, agent id, source id, and stable document id. Declaration version and current path are mutable operational metadata, so prompt/contract upgrades and confident renames preserve the same conversation. Every binding creation or meaningful metadata change appends sensitive `stream.thought.runtime.letta-conversation.binding` evidence. A rebuildable projection stores the latest binding for fast lookup; no new Jazz table or parallel database is introduced. + +Conversation creation is recoverable across the remote-effect gap. Before creating a conversation, the trusted parent searches the exact agent for the deterministic summary marker. Zero matches permits one create, one match is reused, and multiple matches fail closed. Jazz then appends or verifies the binding event and materializes its projection. A missing or stale projection is rebuilt from immutable binding events. A conflicting agent, document, marker, or conversation id is an integrity failure. A path-only rename appends a new binding version without changing conversation identity. + ## Event identity and source sequence Every producer derives a deterministic UUID-compatible Jazz row id from its stable source id and source-native idempotency key. Examples include: diff --git a/spec/recovery.md b/spec/recovery.md index 79bad4a..c69fb42 100644 --- a/spec/recovery.md +++ b/spec/recovery.md @@ -90,6 +90,7 @@ The UI flags contradictions rather than choosing whichever row looks friendlier. - Unknown, missing, candidate, retired, ambiguous-capability, provider-mismatched, non-allowlisted, cloned, rehydrated, mutated-after-compilation, or unbound model adapter: reject declaration registration, run creation, and provider dispatch. Complete canonical compiler-bound identity is checked at every boundary. After read-only prefetch and local broker validation, invoke the opaque run-bound authority capability at the actual provider boundary. It revalidates the exact canonical active binding and holds the token-owned cross-process lock plus lease through the complete bounded provider request. Retirement cannot pass that bound; stale active processes fail before egress. A previously started run keeps its recorded adapter identity through recovery. - Authorization/configuration: block until configuration changes. - Letta Agent SDK `success: false`: settle the attempt's conservative accounting charge, classify the remote failure, and leave source progress unchanged. A later retry first reconciles the deterministic turn marker before sending. +- Per-document Letta conversation binding: derive one content-dark summary marker from declaration, agent, source, and stable document id; reuse one exact remote match, create only on zero matches, and fail closed on multiple matches. Jazz then inserts or verifies the mapping before the turn marker is reconciled. - Inference budget exhaustion: terminally block the attempt before provider dispatch. `onExhaustion: advance` continues from durable consumer progress; `onExhaustion: defer` leaves progress unchanged until the limiting windows permit a later attempt. - Permanent source deletion or malformed source: append an observation/error and advance only according to connector policy. diff --git a/spec/security.md b/spec/security.md index 60a2ff6..9911e27 100644 --- a/spec/security.md +++ b/spec/security.md @@ -61,6 +61,10 @@ The `letta-cloud-v1` adapter gives a Letta agent broad authority inside an SDK-m An unrestricted Cloud permission mode authorizes the Letta harness to use its available sandbox tools without per-call ThoughtStream approval. It does not authorize Telegram delivery, public posting, deployment, account mutation, or any other ThoughtStream egress. Those remain separate trusted actions with their own policies and receipts. Server-side tools or secrets attached directly to the Cloud agent are a separate operator capability and cannot be inferred from the declaration. +The local Agent SDK profile is a named exception only when the SDK-owned App Server successfully applies `filesystemConfinement: memory` to the existing agent memory root, the session has `allowedTools: []`, `skillSources: []`, no MCP servers, no workspace resource, no arbitrary cwd, and strict permission mode. If the kernel confinement primitive, memory root, App Server, or authenticated API-backed agent route is unavailable, the turn fails closed before source progress advances. This profile does not authorize a generic local Agent SDK declaration or a host-workspace coding agent. + +The Coil Public Knowledge trusted parent may read only the configured Coil root, exact Jazz document version, default-deny policy file, and public catalog root. Those paths are operator configuration and never model-selected. It never sends policy-blocked source content to the SDK. Catalog reads accept regular Markdown files inside the configured public root and expose frontmatter only. Recommendations remain sensitive/private and have no direct effect capability. + The resident's mixed Telegram/ATProto conversation makes the ThoughtStream-to-agent border load-bearing. Public source text and third-party Markdown are bounded, snapshotted, and marked as untrusted data; strong references remain distinguishable from mutable protocol or social renderings. The trusted parent calls only the source-appropriate fixed public services: Bluesky post/like context may use atproto.md plus bsky.md, while Semble collection-link context uses atproto.md for the link, card, and collection and never sends those records to bsky.md. ThoughtStream does not pass source credentials, Jazz credentials, deploy keys, Git credentials, host paths, or public-write authority into the packet. Fetched bodies and context snapshots live under private runtime storage and are forbidden from Git, build artifacts, traces, accounting, Telegram delivery, operational errors, and public projections. Prompt guidance reminds the resident not to expose private continuity, but the Cloud sandbox remains an operator-selected capable-agent environment after that border. ## Web authentication containment diff --git a/spec/testing.md b/spec/testing.md index 43af501..e6463f6 100644 --- a/spec/testing.md +++ b/spec/testing.md @@ -98,6 +98,9 @@ These are capability gates, not aspirational checks. An API named `transaction`, - Timeout, Cloud sandbox expiry, protocol failure, terminal SDK failure, stream-without-result, invalid JSON, oversized conversation text, and history reconciliation ambiguity produce classified failures with the correct progress policy. - Cloud credit exhaustion is classified from process-local provider detail as `letta-cloud-insufficient-credits`, leaves progress unchanged, settles the inference reservation conservatively, and persists neither the provider detail nor source text. - A live Cloud canary is opt-in and credentialed. It must use a dedicated hidden agent, a disposable conversation or explicit canary agent main conversation, no private source payload, and no ThoughtStream production activation. It records backend, package version, agent/conversation/run ids, terminal result, and sandbox lifecycle without printing credentials or raw internal reasoning. +- Coil Public Knowledge tests scan a synthetic vault into a temporary Jazz database, preserve one `documentId` and conversation across renames and declaration-version upgrades, skip blocked/deferred/deleted paths before SDK session creation and accounting, bind one conversation per eligible document, recover a summary-marked remote conversation after a simulated pre-Jazz crash, reject duplicate remote markers, and verify unchanged scans make no model call. +- The recommendation contract tests reject unknown revision targets, colliding new slugs, malformed candidate outlines, privacy-clear claims with blocked links, and source/catalog/hash mismatches. Accepted output settles one private recommendation, run lifecycle, conversation binding, accounting record, and source progress. +- Local-memory Agent SDK tests verify `backend=local`, API-backed App Server selection, `filesystemConfinement=memory`, exact memory-root environment, empty skills/tools, strict permission mode, conversation-scoped model application, and absence of Coil/site paths or source text from durable traces. A live local canary is opt-in, uses Co's existing agent and one explicit synthetic or approved file conversation, and cannot activate the full Coil backlog. ## Container-harness security gate diff --git a/spec/vision.md b/spec/vision.md index 5f3c3b1..c2abc6e 100644 --- a/spec/vision.md +++ b/spec/vision.md @@ -24,6 +24,7 @@ The system should answer: - Cameron can watch root activity and inspect complete lineage without being buried in raw trace noise. - Model behavior can improve through versioned prompts, adapters, and Tinker training while old outputs remain attributable to their exact runtime. - A conceptualizer consumer can extract concepts and directional links from source events, producing private derived observations that are inspectable and rebuildable without any outbound publication authority. +- A policy-gated Coil consumer can keep one Co conversation per stable document, recommending revisions or additions to Public Knowledge without gaining file-write, staging, publication, protocol, or deployment authority. ## Explicit non-goals for the first release diff --git a/src/agents/declarations.ts b/src/agents/declarations.ts index bc5603d..77bd2ad 100644 --- a/src/agents/declarations.ts +++ b/src/agents/declarations.ts @@ -9,6 +9,8 @@ import { createOutputContractRegistry, OBSERVATION_OUTPUT_CONTRACT_ID, OBSERVATION_OUTPUT_CONTRACT_VERSION, + PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT_ID, + PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT_VERSION, } from "./output-contracts.js"; import { BUILTIN_PROVIDER_PROFILE_IDS, providerKindForProfile } from "./provider-profiles.js"; import { AGENT_TOOL_NAMES } from "./tools.js"; @@ -126,12 +128,14 @@ const piRunnerSchema = z.object({ const lettaAgentRunnerSchema = z.object({ kind: z.literal("letta-agent-sdk"), - backend: z.literal("cloud"), + backend: z.enum(["cloud", "local"]), agentIdEnv: z.string().regex(/^[A-Z_][A-Z0-9_]*$/, "agentIdEnv must be an environment variable name"), - conversation: z.literal("main").default("main"), + conversation: z.enum(["main", "per-document"]).default("main"), responseMode: z.enum(["strict-json", "conversation-text"]).default("strict-json"), - permissionMode: z.enum(["standard", "acceptEdits", "unrestricted"]).default("unrestricted"), + permissionMode: z.enum(["standard", "acceptEdits", "unrestricted", "strict"]).default("unrestricted"), skillSources: z.array(z.enum(["bundled", "global", "agent", "project"])).max(4).optional(), + memoryDirEnv: z.string().regex(/^[A-Z_][A-Z0-9_]*$/, "memoryDirEnv must be an environment variable name").optional(), + reasoningEffort: z.enum(["none", "minimal", "low", "medium", "high", "xhigh"]).optional(), dreaming: z.object({ trigger: z.enum(["off", "step-count", "compaction-event"]).default("off"), stepCount: z.number().int().positive().max(10_000).optional(), @@ -149,6 +153,16 @@ const lettaAgentRunnerSchema = z.object({ if (value.dreaming.trigger !== "step-count" && value.dreaming.stepCount !== undefined) { context.addIssue({ code: "custom", path: ["dreaming", "stepCount"], message: "stepCount is valid only for step-count dreaming" }); } + if (value.backend === "local") { + if (!value.memoryDirEnv) context.addIssue({ code: "custom", path: ["memoryDirEnv"], message: "Local Agent SDK sessions require a memory-root environment reference" }); + if (value.permissionMode !== "strict") context.addIssue({ code: "custom", path: ["permissionMode"], message: "Local Agent SDK sessions require strict permission mode" }); + if ((value.skillSources ?? []).length > 0) context.addIssue({ code: "custom", path: ["skillSources"], message: "Local Agent SDK sessions must disable skills" }); + if (value.conversation !== "per-document") context.addIssue({ code: "custom", path: ["conversation"], message: "The local-memory Agent SDK profile is limited to per-document conversations" }); + if (value.responseMode !== "strict-json") context.addIssue({ code: "custom", path: ["responseMode"], message: "Per-document local Agent SDK sessions require strict JSON" }); + if (value.dreaming.trigger !== "off") context.addIssue({ code: "custom", path: ["dreaming"], message: "Per-document local Agent SDK sessions disable dreaming" }); + } else if (value.memoryDirEnv) { + context.addIssue({ code: "custom", path: ["memoryDirEnv"], message: "Cloud Agent SDK sessions cannot select a host memory root" }); + } }); const declarationFileSchema = z.object({ @@ -173,7 +187,7 @@ const declarationFileSchema = z.object({ context: z.object({ maxEvents: z.number().int().positive().max(100).default(1), maxChars: z.number().int().positive().max(1_000_000).default(64_000), - strategy: z.enum(["single-event", "telegram-conversation", "atproto-batch"]).default("single-event"), + strategy: z.enum(["single-event", "telegram-conversation", "atproto-batch", "coil-public-knowledge"]).default("single-event"), documents: contextDocumentsSchema.optional(), historyAgentIds: z.array(declarationIdSchema).min(1).max(32).optional(), payloadFields: z.array(z.string().min(1).max(100)).min(1).max(100).optional(), @@ -203,6 +217,7 @@ const declarationFileSchema = z.object({ externalActions: z.literal(false), }).strict(), enabled: z.boolean().default(true), + enabledEnv: z.string().regex(/^[A-Z_][A-Z0-9_]*$/, "enabledEnv must be an environment variable name").optional(), }).strict().superRefine((value, context) => { const conceptualizationOutput = value.outputContract.id === CONCEPTUALIZATION_OUTPUT_CONTRACT_ID; const conceptGraphEvent = value.emit[0] === "stream.thought.derived.concept.graph"; @@ -353,7 +368,7 @@ const declarationFileSchema = z.object({ }); } if (value.runner.kind === "letta-agent-sdk") { - if (!["single-event", "atproto-batch"].includes(value.context.strategy) || value.context.maxEvents !== 1) { + if (!["single-event", "atproto-batch", "coil-public-knowledge"].includes(value.context.strategy) || value.context.maxEvents !== 1) { context.addIssue({ code: "custom", path: ["context"], @@ -367,6 +382,34 @@ const declarationFileSchema = z.object({ message: "Letta Agent SDK declarations require one to eight concrete source namespaces", }); } + const publicKnowledge = value.context.strategy === "coil-public-knowledge"; + const fileTypes = new Set([ + "stream.thought.source.file.added", + "stream.thought.source.file.changed", + "stream.thought.source.file.renamed", + "stream.thought.source.file.deleted", + ]); + if (publicKnowledge && ( + value.runner.conversation !== "per-document" + || value.runner.responseMode !== "strict-json" + || value.outputContract.id !== PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT_ID + || value.outputContract.version !== PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT_VERSION + || value.emit[0] !== "stream.thought.derived.public-knowledge.recommendation" + || value.subscribe.sources.length !== 1 + || value.subscribe.privacy.length !== 1 + || value.subscribe.privacy[0] !== "sensitive" + || value.subscribe.types.length < 1 + || value.subscribe.types.some((type) => !fileTypes.has(type)) + )) { + context.addIssue({ + code: "custom", + path: ["context", "strategy"], + message: "Coil Public Knowledge context requires one sensitive filesystem source, per-document strict JSON, and the canonical recommendation contract/event", + }); + } + if (!publicKnowledge && value.runner.conversation === "per-document") { + context.addIssue({ code: "custom", path: ["runner", "conversation"], message: "Per-document conversations require Coil Public Knowledge context" }); + } } if (value.runner.kind === "pi" && value.runner.outputMode === "conversation-text" && ( value.role !== "standard" @@ -429,6 +472,7 @@ export async function loadAgentDeclarations( const raw = await fs.readFile(filePath, "utf8"); const parsed = entry.name.endsWith(".json") ? JSON.parse(raw) : YAML.parse(raw); const file = declarationFileSchema.parse(parsed); + const effectiveEnabled = file.enabled || enabledByEnvironment(file.enabledEnv, environment); for (const type of file.emit) { if (!registry.has(type, 1)) throw new Error(`Agent ${file.id} emits an unregistered event type: ${type}@1`); } @@ -462,9 +506,9 @@ export async function loadAgentDeclarations( if (file.runner.kind === "pi" && file.runner.adapter && (file.runner.model || file.runner.tier)) { throw new Error(`Agent ${file.id}@${file.version} cannot combine adapter selection with model or tier`); } - const concreteModel = selectedAdapter?.baseModel ?? resolveRunnerModel(file.runner, environment, file.enabled); + const concreteModel = selectedAdapter?.baseModel ?? resolveRunnerModel(file.runner, environment, effectiveEnabled); const lettaAgentId = file.runner.kind === "letta-agent-sdk" - ? resolveLettaAgentId(file.runner.agentIdEnv, environment, file.enabled) + ? resolveLettaAgentId(file.runner.agentIdEnv, environment, effectiveEnabled) : undefined; const outputContract = outputContracts.get(file.outputContract.id, file.outputContract.version).identity; const declaration: ThoughtAgentDeclaration = { @@ -486,7 +530,7 @@ export async function loadAgentDeclarations( adapterCatalogGeneration: adapterCatalog!.generation, } : {}), ...(file.runner.kind === "letta-agent-sdk" ? { - provider: "letta-cloud" as const, + provider: file.runner.backend === "cloud" ? "letta-cloud" as const : "letta-local" as const, lettaAgent: { backend: file.runner.backend, agentIdEnv: file.runner.agentIdEnv, @@ -495,6 +539,8 @@ export async function loadAgentDeclarations( responseMode: file.runner.responseMode, permissionMode: file.runner.permissionMode, ...(file.runner.skillSources ? { skillSources: file.runner.skillSources } : {}), + ...(file.runner.memoryDirEnv ? { memoryDirEnv: file.runner.memoryDirEnv } : {}), + ...(file.runner.reasoningEffort ? { reasoningEffort: file.runner.reasoningEffort } : {}), dreaming: file.runner.dreaming, sandbox: file.runner.sandbox, }, @@ -511,7 +557,7 @@ export async function loadAgentDeclarations( emit: file.emit, promptRef: file.prompt, systemPrompt, - enabled: file.enabled, + enabled: effectiveEnabled, maxEvents: file.context.maxEvents, maxInputChars: file.context.maxChars, contextStrategy: file.context.strategy, @@ -572,7 +618,15 @@ function resolveLettaAgentId( return configured; } if (!enabled) return undefined; - throw new Error(`No Letta Cloud agent configured; set ${environmentKey}`); + throw new Error(`No Letta agent configured; set ${environmentKey}`); +} + +function enabledByEnvironment(environmentKey: string | undefined, environment: NodeJS.ProcessEnv): boolean { + if (!environmentKey) return false; + const value = environment[environmentKey]?.trim().toLowerCase(); + if (!value || value === "0" || value === "false") return false; + if (value === "1" || value === "true") return true; + throw new Error(`${environmentKey} must be one of 1, true, 0, or false`); } export function declarationFingerprint(declaration: ThoughtAgentDeclaration): string { diff --git a/src/agents/letta-agent-sdk.ts b/src/agents/letta-agent-sdk.ts index df848b0..df0cd5e 100644 --- a/src/agents/letta-agent-sdk.ts +++ b/src/agents/letta-agent-sdk.ts @@ -3,6 +3,7 @@ import { LettaAgentClient, type LettaCodeClientSessionOptions, type LettaCodeSession, + type LettaConversation, type ListMessagesResult, type SDKMessage, type SDKResultMessage, @@ -11,11 +12,14 @@ import { createHash } from "node:crypto"; import { stableKey } from "../core/ids.js"; import type { JsonObject } from "../core/json.js"; import type { InferenceUsage } from "../store/types.js"; +import type { LettaConversationBinding } from "../store/types.js"; import { createOutputContractRegistry, outputContractForDeclaration, outputContractIdentityJson, OutputContractValidationError, + PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT_ID, + type PublicKnowledgeRecommendationOutput, type ObservationOutput, type OutputContractRegistry, } from "./output-contracts.js"; @@ -27,9 +31,10 @@ import { type RunnerTrace, type ThoughtAgentDeclaration, } from "./types.js"; +import { validatePublicKnowledgeRecommendationAgainstContext } from "../public-knowledge/context.js"; -export const LETTA_AGENT_SDK_ADAPTER_REVISION = "letta-agent-sdk-cloud-v1"; -export const LETTA_AGENT_SDK_PACKAGE_VERSION = "0.2.6"; +export const LETTA_AGENT_SDK_ADAPTER_REVISION = "letta-agent-sdk-conversations-v2"; +export const LETTA_AGENT_SDK_PACKAGE_VERSION = "0.5.7"; const MAX_FINAL_JSON_CHARS = 64_000; const MAX_CONVERSATION_TEXT_CHARS = 2_000; @@ -42,7 +47,24 @@ const MAX_USAGE_RUN_IDS = 20; const RUN_ID_PATTERN = /^run-[A-Za-z0-9-]{1,200}$/; export interface LettaAgentSdkClient { + createSession?(id: string, options?: LettaCodeClientSessionOptions): LettaCodeSession; resumeSession(id: string, options?: LettaCodeClientSessionOptions): LettaCodeSession; + conversations?: { + list(options?: { agentId?: string; limit?: number; summarySearch?: string }): Promise; + create(options: { + agentId: string; + summary?: string; + description?: string; + model?: string; + hidden?: boolean; + }): Promise; + retrieve(conversationId: string): Promise; + }; +} + +export interface LettaConversationBindingStore { + getLettaConversationBinding(id: string): Promise; + upsertLettaConversationBinding(binding: LettaConversationBinding): Promise; } export interface LettaRunUsageClient { @@ -59,6 +81,7 @@ export interface LettaAgentSdkRunnerOptions { outputContracts?: OutputContractRegistry; runUsageClient?: LettaRunUsageClient; fetch?: typeof fetch; + conversationStore?: LettaConversationBindingStore | undefined; } type HistoryReconciliation = @@ -80,7 +103,7 @@ export class LettaAgentSdkRunner implements AgentRunner { private readonly historyPages: number; private readonly reconciliationDelayMs: number; private readonly sleep: (milliseconds: number) => Promise; - private client: LettaAgentSdkClient | undefined; + private readonly clients = new Map<"cloud" | "local", LettaAgentSdkClient>(); private runUsageClient: LettaRunUsageClient | undefined; private runUsageClientResolved = false; @@ -95,14 +118,17 @@ export class LettaAgentSdkRunner implements AgentRunner { if (!Number.isSafeInteger(this.reconciliationDelayMs) || this.reconciliationDelayMs < 0 || this.reconciliationDelayMs > 30_000) { throw new Error("reconciliationDelayMs must be an integer from 0 through 30000"); } - this.client = options.client; + if (options.client) { + this.clients.set("cloud", options.client); + this.clients.set("local", options.client); + } this.runUsageClient = options.runUsageClient; } async run(input: AgentRunInput, onTrace: (trace: RunnerTrace) => Promise): Promise { const declaration = input.declaration; const config = declaration.lettaAgent; - if (!config || config.backend !== "cloud" || config.conversation !== "main" || !config.agentId) { + if (!config || !["cloud", "local"].includes(config.backend) || !config.agentId) { throw new AgentRunFailure("Letta Agent SDK declaration is not runnable", { advanceProgress: false, diagnostic: { @@ -114,9 +140,11 @@ export class LettaAgentSdkRunner implements AgentRunner { }); } - const client = this.resolveClient(); + const client = this.resolveClient(declaration); const turnKey = lettaTurnKey(declaration, input.event.id); - const session = client.resumeSession(config.agentId, sessionOptions(declaration)); + const options = sessionOptions(declaration, this.options.environment ?? process.env); + const conversation = await this.resolveConversation(client, input); + const session = client.resumeSession(conversation.sessionId, options); let sent = false; let acceptTraces = true; const emitTrace = async (trace: RunnerTrace): Promise => { @@ -127,11 +155,12 @@ export class LettaAgentSdkRunner implements AgentRunner { await emitTrace({ kind: "letta.session.opened", data: { - backend: "cloud", + backend: config.backend, executionAdapterRevision: LETTA_AGENT_SDK_ADAPTER_REVISION, sdkVersion: LETTA_AGENT_SDK_PACKAGE_VERSION, agentId: config.agentId!, - conversation: "main", + conversation: config.conversation, + ...(conversation.bindingId ? { bindingId: conversation.bindingId } : {}), }, }); @@ -146,7 +175,7 @@ export class LettaAgentSdkRunner implements AgentRunner { text: textMetadata(reconciliation.text), }, }); - return this.parseOutput(declaration, reconciliation.text); + return this.parseOutput(input, reconciliation.text); } if (reconciliation.status === "pending") { reconciliation = await this.delayedReconciliation(session, turnKey); @@ -159,7 +188,7 @@ export class LettaAgentSdkRunner implements AgentRunner { text: textMetadata(reconciliation.text), }, }); - return this.parseOutput(declaration, reconciliation.text); + return this.parseOutput(input, reconciliation.text); } if (reconciliation.status === "inconclusive") throw inconclusiveHistoryFailure(session, turnKey); throw pendingTurnFailure(session, turnKey); @@ -176,7 +205,7 @@ export class LettaAgentSdkRunner implements AgentRunner { text: textMetadata(reconciliation.text), }, }); - return this.parseOutput(declaration, reconciliation.text); + return this.parseOutput(input, reconciliation.text); } if (reconciliation.status === "inconclusive") throw inconclusiveHistoryFailure(session, turnKey); if (reconciliation.status === "pending") throw pendingTurnFailure(session, turnKey); @@ -219,14 +248,14 @@ export class LettaAgentSdkRunner implements AgentRunner { }, }); } - const usage = await this.reportedUsage(result, emitTrace); + const usage = await this.reportedUsage(result, emitTrace, declaration); if (!result.success) throw terminalSdkFailure(result, turnKey, usage); if (typeof result.result !== "string" || result.result.trim().length === 0) { throw invalidSdkOutput("empty-final-text", result.result ?? "", declaration, undefined, usage); } let output: AgentOutput; try { - output = this.parseOutput(declaration, result.result); + output = this.parseOutput(input, result.result); } catch (error) { if (error instanceof AgentRunFailure && usage) { throw new AgentRunFailure(error.message, { @@ -286,9 +315,104 @@ export class LettaAgentSdkRunner implements AgentRunner { } } - private resolveClient(): LettaAgentSdkClient { - if (this.client) return this.client; + private async resolveConversation( + client: LettaAgentSdkClient, + input: AgentRunInput, + ): Promise<{ sessionId: string; bindingId?: string }> { + const config = input.declaration.lettaAgent!; + if (config.conversation === "main") return { sessionId: config.agentId! }; + const store = this.options.conversationStore; + if (!store || !client.conversations) { + throw new AgentRunFailure("Per-document Letta conversation storage is unavailable", { + advanceProgress: false, + diagnostic: { code: "letta-conversation-store-missing", stage: "sdk-setup" }, + }); + } + const documentId = requiredEventString(input.event, "documentId"); + const currentPath = requiredEventString(input.event, "path"); + const bindingId = lettaDocumentConversationBindingId(input.declaration, input.event); + const remoteMarker = lettaDocumentConversationRemoteMarker(bindingId); + const existing = await store.getLettaConversationBinding(bindingId); + if (existing) { + const remote = await client.conversations.retrieve(existing.conversationId); + if (remote.id !== existing.conversationId + || remote.agent_id !== existing.agentId + || remote.summary !== existing.remoteMarker + || existing.remoteMarker !== remoteMarker) { + throw new AgentRunFailure("Per-document Letta conversation binding no longer matches remote state", { + advanceProgress: false, + diagnostic: { code: "letta-conversation-binding-drift", stage: "sdk-setup", bindingId }, + }); + } + await store.upsertLettaConversationBinding({ + ...existing, + declarationVersion: input.declaration.version, + currentPath, + updatedAt: new Date().toISOString(), + }); + return { sessionId: existing.conversationId, bindingId }; + } + + const matches = (await client.conversations.list({ + agentId: config.agentId!, + summarySearch: remoteMarker, + limit: 10, + })).filter((conversation) => ( + conversation.summary === remoteMarker + && conversation.agent_id === config.agentId + )); + if (matches.length > 1) { + throw new AgentRunFailure("Per-document Letta conversation marker is ambiguous", { + advanceProgress: false, + diagnostic: { + code: "letta-conversation-marker-ambiguous", + stage: "sdk-setup", + bindingId, + matchCount: matches.length, + }, + }); + } + const conversation = matches[0] ?? await client.conversations.create({ + agentId: config.agentId!, + summary: remoteMarker, + description: "ThoughtStream private file-scoped Public Knowledge recommendation conversation.", + ...(input.declaration.model ? { model: input.declaration.model } : {}), + hidden: true, + }); + if (!conversation.id || conversation.agent_id !== config.agentId) { + throw new AgentRunFailure("Per-document Letta conversation identity is invalid", { + advanceProgress: false, + diagnostic: { code: "letta-conversation-identity-invalid", stage: "sdk-setup", bindingId }, + }); + } + const now = new Date().toISOString(); + await store.upsertLettaConversationBinding({ + id: bindingId, + declarationId: input.declaration.id, + declarationVersion: input.declaration.version, + agentId: config.agentId!, + source: input.event.source, + scopeType: "document", + scopeKey: documentId, + conversationId: conversation.id, + remoteMarker, + currentPath, + createdAt: now, + updatedAt: now, + }); + return { sessionId: conversation.id, bindingId }; + } + + private resolveClient(declaration: ThoughtAgentDeclaration): LettaAgentSdkClient { + const backend = declaration.lettaAgent!.backend; + const cached = this.clients.get(backend); + if (cached) return cached; const environment = this.options.environment ?? process.env; + if (backend === "local") { + const client = new LettaAgentClient({ backend: "local", appServer: { harnessBackend: "api" } }); + this.clients.set("local", client); + return client; + } const apiKey = this.options.apiKey ?? environment.LETTA_API_KEY; if (!apiKey) { throw new AgentRunFailure("Letta Cloud API credential is unavailable", { @@ -296,13 +420,15 @@ export class LettaAgentSdkRunner implements AgentRunner { diagnostic: { code: "letta-cloud-credential-missing", stage: "sdk-setup" }, }); } - this.client = new LettaAgentClient({ backend: "cloud", apiKey }); - return this.client; + const client = new LettaAgentClient({ backend: "cloud", apiKey }); + this.clients.set("cloud", client); + return client; } private async reportedUsage( result: SDKResultMessage, emitTrace: (trace: RunnerTrace) => Promise, + declaration: ThoughtAgentDeclaration, ): Promise { const usage: InferenceUsage = {}; const costMicrousd = usdToMicrousd(result.totalCostUsd); @@ -311,7 +437,7 @@ export class LettaAgentSdkRunner implements AgentRunner { const sdkRunIds = [...new Set(result.runIds ?? [])]; const validRunIds = sdkRunIds.filter((runId) => RUN_ID_PATTERN.test(runId)); const runSetComplete = validRunIds.length === sdkRunIds.length && validRunIds.length <= MAX_USAGE_RUN_IDS; - const client = this.resolveRunUsageClient(); + const client = declaration.lettaAgent?.backend === "cloud" ? this.resolveRunUsageClient() : undefined; let retrievedRunCount = 0; if (client && runSetComplete && validRunIds.length > 0) { const results = await Promise.all(validRunIds.map(async (runId) => { @@ -367,7 +493,8 @@ export class LettaAgentSdkRunner implements AgentRunner { return reconcileHistory(session, turnKey, this.historyPages); } - private parseOutput(declaration: ThoughtAgentDeclaration, text: string): AgentOutput { + private parseOutput(input: AgentRunInput, text: string): AgentOutput { + const declaration = input.declaration; const identity = outputContractForDeclaration(declaration); const config = declaration.lettaAgent!; let parsed: AgentOutput; @@ -402,11 +529,21 @@ export class LettaAgentSdkRunner implements AgentRunner { if (!(error instanceof OutputContractValidationError)) throw error; throw invalidSdkOutput("output-contract-invalid", text, declaration, error); } + if (identity.id === PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT_ID) { + try { + validatePublicKnowledgeRecommendationAgainstContext( + parsed as PublicKnowledgeRecommendationOutput, + input.context, + ); + } catch { + throw invalidSdkOutput("context-bound-output-invalid", text, declaration); + } + } } return { ...parsed, model: { - provider: "letta-cloud", + provider: declaration.provider ?? `letta-${config.backend}`, id: declaration.model ?? "agent-default", revision: `${LETTA_AGENT_SDK_PACKAGE_VERSION}/${LETTA_AGENT_SDK_ADAPTER_REVISION}`, }, @@ -440,6 +577,25 @@ export function lettaTurnKey(declaration: ThoughtAgentDeclaration, eventId: stri return stableKey("letta-agent-sdk-turn", declaration.id, String(declaration.version), eventId); } +export function lettaDocumentConversationBindingId( + declaration: ThoughtAgentDeclaration, + event: AgentRunInput["event"], +): string { + const agentId = declaration.lettaAgent?.agentId; + if (!agentId) throw new Error("Document conversation binding requires a resolved agent id"); + return stableKey( + "letta-conversation", + declaration.id, + agentId, + event.source, + requiredEventString(event, "documentId"), + ); +} + +export function lettaDocumentConversationRemoteMarker(bindingId: string): string { + return `thoughtstream:coil:${createHash("sha256").update(bindingId).digest("hex")}`; +} + export function buildLettaTurnMessage( input: AgentRunInput, turnKey: string, @@ -690,8 +846,31 @@ class SdkTraceAccumulator { } } -function sessionOptions(declaration: ThoughtAgentDeclaration): LettaCodeClientSessionOptions { +function sessionOptions( + declaration: ThoughtAgentDeclaration, + environment: NodeJS.ProcessEnv, +): LettaCodeClientSessionOptions { const config = declaration.lettaAgent!; + if (config.backend === "local") { + const memoryDir = config.memoryDirEnv ? environment[config.memoryDirEnv]?.trim() : undefined; + if (!memoryDir || !memoryDir.startsWith("/")) { + throw new AgentRunFailure("Local Agent SDK memory root is unavailable", { + advanceProgress: false, + diagnostic: { code: "letta-local-memory-root-missing", stage: "sdk-setup" }, + }); + } + return { + permissionMode: "strict", + skillSources: [], + allowedTools: [], + dreaming: { trigger: "off" }, + cwd: memoryDir, + env: { MEMORY_DIR: memoryDir }, + filesystemConfinement: "memory", + ...(declaration.model ? { model: declaration.model } : {}), + ...(config.reasoningEffort ? { reasoningEffort: config.reasoningEffort } : {}), + }; + } return { permissionMode: config.permissionMode, ...(config.skillSources ? { skillSources: config.skillSources } : {}), @@ -701,9 +880,21 @@ function sessionOptions(declaration: ThoughtAgentDeclaration): LettaCodeClientSe }, sandbox: config.sandbox, ...(declaration.model ? { model: declaration.model } : {}), + ...(config.reasoningEffort ? { reasoningEffort: config.reasoningEffort } : {}), }; } +function requiredEventString(event: AgentRunInput["event"], field: string): string { + const value = event.payload[field]; + if (typeof value !== "string" || !value) { + throw new AgentRunFailure(`Per-document Letta event ${field} is invalid`, { + advanceProgress: true, + diagnostic: { code: "letta-document-scope-invalid", stage: "sdk-setup", field }, + }); + } + return value; +} + function pendingTurnFailure(session: LettaCodeSession, turnKey: string): AgentRunFailure { return new AgentRunFailure("Letta Cloud turn exists without a terminal assistant result", { advanceProgress: false, diff --git a/src/agents/output-contracts.ts b/src/agents/output-contracts.ts index 0ddaa10..8e1d91a 100644 --- a/src/agents/output-contracts.ts +++ b/src/agents/output-contracts.ts @@ -10,6 +10,10 @@ export const CONCEPTUALIZATION_OUTPUT_CONTRACT_VERSION = 1; export const REVIEW_RESPONSE_OUTPUT_CONTRACT_ID = "stream.thought.output.review-response"; export const REVIEW_RESPONSE_OUTPUT_CONTRACT_VERSION = 1; +export const PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT_ID = "stream.thought.output.public-knowledge-recommendation"; +export const PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT_V1_VERSION = 1; +export const PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT_VERSION = 2; + const recommendationSchema = z.object({ target: z.string().min(1).max(200), reason: z.string().min(1).max(2_000), @@ -89,7 +93,91 @@ export const reviewResponseOutputSchema = z.object({ })); export type ReviewResponseOutput = z.infer; -export type SemanticOutput = ObservationOutput | ConceptualizationOutput | ReviewResponseOutput; + +const publicKnowledgeCandidateSchemaV1 = z.object({ + slug: z.string().min(1).max(160).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/), + title: z.string().min(1).max(240), + summary: z.string().min(1).max(1_600), + outline: z.array(z.string().min(1).max(240)).min(1).max(24), +}).strict(); + +export const publicKnowledgeRecommendationOutputSchemaV1 = z.object({ + decision: z.enum(["propose-new", "revise-existing", "skip"]), + summary: z.string().min(1).max(1_600), + rationale: z.string().min(1).max(4_000), + candidate: publicKnowledgeCandidateSchemaV1.nullable(), + targetSlugs: z.array(z.string().min(1).max(160)).max(24), + publicSourcesToVerify: z.array(z.string().min(1).max(500)).max(24), + privacy: z.object({ + status: z.enum(["clear", "review", "block"]), + findings: z.array(z.string().min(1).max(500)).max(20), + }).strict(), + confidence: z.number().min(0).max(1), +}).strict().superRefine((value, context) => { + if (value.decision === "propose-new") { + if (!value.candidate) { + context.addIssue({ code: "custom", path: ["candidate"], message: "A new entry proposal requires a candidate" }); + } + if (value.targetSlugs.length > 0) { + context.addIssue({ code: "custom", path: ["targetSlugs"], message: "A new entry proposal cannot name revision targets" }); + } + } + if (value.decision === "revise-existing" && value.targetSlugs.length === 0) { + context.addIssue({ code: "custom", path: ["targetSlugs"], message: "A revision requires at least one existing target" }); + } + if (value.decision === "revise-existing" && value.candidate) { + context.addIssue({ code: "custom", path: ["candidate"], message: "A revision names targets rather than creating a candidate" }); + } + if (value.decision === "skip" && (value.candidate || value.targetSlugs.length > 0)) { + context.addIssue({ code: "custom", path: ["decision"], message: "A skipped recommendation cannot include a candidate or targets" }); + } +}); + +const publicKnowledgeCandidateSchema = z.object({ + slug: z.string().min(1).max(160).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/), + title: z.string().min(1).max(240), + summary: z.string().min(1).max(1_600), + kind: z.string().min(1).max(100), + claimOutline: z.array(z.string().min(1).max(500)).min(1).max(24), + publicSourcesNeeded: z.array(z.string().min(1).max(500)).max(24), + privateDependencies: z.array(z.string().min(1).max(500)).max(24), + relatedSlugs: z.array(z.string().min(1).max(160)).max(24), +}).strict(); + +export const publicKnowledgeRecommendationOutputSchema = z.object({ + decision: z.enum(["propose-new", "revise-existing", "skip"]), + summary: z.string().min(1).max(1_600), + rationale: z.array(z.string().min(1).max(1_000)).min(1).max(20), + candidate: publicKnowledgeCandidateSchema.nullable(), + targetSlugs: z.array(z.string().min(1).max(160)).max(24), + publicSourcesToVerify: z.array(z.string().min(1).max(500)).max(24), + privacy: z.object({ + status: z.enum(["clear", "review", "blocked"]), + findings: z.array(z.string().min(1).max(500)).max(20), + }).strict(), + confidence: z.enum(["low", "medium", "high"]), +}).strict().superRefine((value, context) => { + if (value.decision === "propose-new") { + if (!value.candidate) { + context.addIssue({ code: "custom", path: ["candidate"], message: "A new entry proposal requires a candidate" }); + } + if (value.targetSlugs.length > 0) { + context.addIssue({ code: "custom", path: ["targetSlugs"], message: "A new entry proposal cannot name revision targets" }); + } + } + if (value.decision === "revise-existing" && value.targetSlugs.length === 0) { + context.addIssue({ code: "custom", path: ["targetSlugs"], message: "A revision requires at least one existing target" }); + } + if (value.decision === "revise-existing" && value.candidate) { + context.addIssue({ code: "custom", path: ["candidate"], message: "A revision names targets rather than creating a candidate" }); + } + if (value.decision === "skip" && (value.candidate || value.targetSlugs.length > 0)) { + context.addIssue({ code: "custom", path: ["decision"], message: "A skipped recommendation cannot include a candidate or targets" }); + } +}); + +export type PublicKnowledgeRecommendationOutput = z.infer; +export type SemanticOutput = ObservationOutput | ConceptualizationOutput | ReviewResponseOutput | PublicKnowledgeRecommendationOutput; export interface OutputContractIdentity { id: string; @@ -145,6 +233,116 @@ export const OBSERVATION_OUTPUT_CONTRACT: OutputContractDefinition> = { + identity: publicKnowledgeRecommendationIdentityV1, + definition: publicKnowledgeRecommendationDefinitionV1, + prompt: "Return exactly one raw JSON object with the required keys decision, summary, rationale, candidate, targetSlugs, publicSourcesToVerify, privacy, and confidence. decision is propose-new, revise-existing, or skip. propose-new requires a complete candidate and no targetSlugs. revise-existing requires existing targetSlugs and candidate null. skip requires candidate null and no targetSlugs. privacy has exactly status and findings. Do not include source content, private names, personal details, Markdown fences, unknown fields, or text outside the JSON object.", + schema: publicKnowledgeRecommendationOutputSchemaV1, +}; + +const publicKnowledgeRecommendationDefinition: JsonObject = { + id: PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT_ID, + version: PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT_VERSION, + type: "object", + unknownFields: "reject", + invariants: [ + "propose-new requires candidate and forbids targetSlugs", + "revise-existing requires one or more targetSlugs and candidate null", + "skip forbids candidate and targetSlugs", + ], + fields: { + decision: { type: "enum", values: ["propose-new", "revise-existing", "skip"], required: true }, + summary: { type: "string", minChars: 1, maxChars: 1_600, required: true }, + rationale: { type: "array", minItems: 1, maxItems: 20, required: true }, + candidate: { + type: ["object", "null"], + required: true, + unknownFields: "reject", + fields: { + slug: { type: "string", minChars: 1, maxChars: 160, pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$", required: true }, + title: { type: "string", minChars: 1, maxChars: 240, required: true }, + summary: { type: "string", minChars: 1, maxChars: 1_600, required: true }, + kind: { type: "string", minChars: 1, maxChars: 100, required: true }, + claimOutline: { type: "array", minItems: 1, maxItems: 24, required: true }, + publicSourcesNeeded: { type: "array", maxItems: 24, required: true }, + privateDependencies: { type: "array", maxItems: 24, required: true }, + relatedSlugs: { type: "array", maxItems: 24, required: true }, + }, + }, + targetSlugs: { type: "array", maxItems: 24, required: true }, + publicSourcesToVerify: { type: "array", maxItems: 24, required: true }, + privacy: { + type: "object", + required: true, + unknownFields: "reject", + fields: { + status: { type: "enum", values: ["clear", "review", "blocked"], required: true }, + findings: { type: "array", maxItems: 20, required: true }, + }, + }, + confidence: { type: "enum", values: ["low", "medium", "high"], required: true }, + }, +}; + +const publicKnowledgeRecommendationIdentity: OutputContractIdentity = { + id: PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT_ID, + version: PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT_VERSION, + sha256: sha256(canonicalJson(publicKnowledgeRecommendationDefinition)), +}; + +export const PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT: OutputContractDefinition = { + identity: publicKnowledgeRecommendationIdentity, + definition: publicKnowledgeRecommendationDefinition, + prompt: "Return exactly one raw JSON object with the required keys decision, summary, rationale, candidate, targetSlugs, publicSourcesToVerify, privacy, and confidence. rationale is an array of concise strings. confidence is exactly low, medium, or high. decision is propose-new, revise-existing, or skip. propose-new requires a complete candidate with slug, title, summary, kind, claimOutline, publicSourcesNeeded, privateDependencies, and relatedSlugs, and no targetSlugs. revise-existing requires existing targetSlugs and candidate null. skip requires candidate null and no targetSlugs. privacy has exactly status (clear, review, or blocked) and findings. Do not include source content, private names, personal details, Markdown fences, unknown fields, or text outside the JSON object.", + schema: publicKnowledgeRecommendationOutputSchema, +}; + export class OutputContractValidationError extends Error { readonly identity: OutputContractIdentity; readonly issues: SanitizedOutputIssue[]; @@ -321,7 +519,9 @@ export function createOutputContractRegistry(): OutputContractRegistry { return new OutputContractRegistry() .register(OBSERVATION_OUTPUT_CONTRACT) .register(CONCEPTUALIZATION_OUTPUT_CONTRACT) - .register(REVIEW_RESPONSE_OUTPUT_CONTRACT); + .register(REVIEW_RESPONSE_OUTPUT_CONTRACT) + .register(PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT_V1) + .register(PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT); } export function defaultOutputContractIdentity(): OutputContractIdentity { diff --git a/src/agents/runtime.ts b/src/agents/runtime.ts index 5818532..60842f1 100644 --- a/src/agents/runtime.ts +++ b/src/agents/runtime.ts @@ -32,6 +32,7 @@ import { CONCEPTUALIZATION_OUTPUT_CONTRACT_ID, createOutputContractRegistry, OBSERVATION_OUTPUT_CONTRACT_ID, + PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT_ID, REVIEW_RESPONSE_OUTPUT_CONTRACT_ID, reviewResponseSummary, outputContractForDeclaration, @@ -41,6 +42,11 @@ import { type OutputContractRegistry, } from "./output-contracts.js"; import { REVIEW_RESPONSE_EVENT_TYPE } from "../review/types.js"; +import { + buildPublicKnowledgeContextPacket, + PublicKnowledgeEligibilitySkip, + type PublicKnowledgeContextOptions, +} from "../public-knowledge/context.js"; import { PiAgentRunner } from "./pi.js"; import { RepairRequestCoordinator } from "./repairs.js"; import { ConsumerScheduler } from "./scheduler.js"; @@ -65,6 +71,7 @@ export interface ThoughtAgentRuntimeOptions { maxConcurrentOperations?: number; reconcileIntervalMs?: number; atprotoObjectContext?: AtprotoObjectContextOptions | undefined; + publicKnowledgeContext?: PublicKnowledgeContextOptions | undefined; } export class ThoughtAgentRuntime { @@ -75,6 +82,7 @@ export class ThoughtAgentRuntime { private readonly maxConcurrentOperations: number; private readonly reconcileIntervalMs: number; private readonly atprotoObjectContext: AtprotoObjectContextOptions; + private readonly publicKnowledgeContext: PublicKnowledgeContextOptions | undefined; constructor( private readonly store: JazzThoughtStore, @@ -84,6 +92,7 @@ export class ThoughtAgentRuntime { this.maxConcurrentOperations = options.maxConcurrentOperations ?? 4; this.reconcileIntervalMs = options.reconcileIntervalMs ?? 1_000; this.atprotoObjectContext = options.atprotoObjectContext ?? {}; + this.publicKnowledgeContext = options.publicKnowledgeContext; if (!Number.isSafeInteger(this.maxConcurrentOperations) || this.maxConcurrentOperations < 1) { throw new Error("Maximum concurrent consumer operations must be a positive integer"); } @@ -95,7 +104,7 @@ export class ThoughtAgentRuntime { const selected = runners ?? [ new DeterministicTriageRunner(), new PiAgentRunner({ artifactRoot: store.getArtifactRoot(), outputContracts: this.outputContracts }), - new LettaAgentSdkRunner({ outputContracts: this.outputContracts }), + new LettaAgentSdkRunner({ outputContracts: this.outputContracts, conversationStore: store }), ]; for (const runner of selected) this.runners.set(runner.mode, runner); } @@ -305,7 +314,15 @@ export class ThoughtAgentRuntime { } const attempt = (prior?.attempt ?? 0) + 1; const runId = stableKey("run", key, String(attempt)); - const context = await this.contextFor(declaration, event); + let context; + try { + context = await this.contextFor(declaration, event); + } catch (error) { + if (error instanceof PublicKnowledgeEligibilitySkip) { + return this.settlePreflightSkip(declaration, event, key, runId, attempt, error); + } + throw error; + } const runner = this.runners.get(declaration.mode); if (!runner) return { declaration, runId, error: `No runner for mode ${declaration.mode}` }; const startedAt = new Date().toISOString(); @@ -733,6 +750,17 @@ export class ThoughtAgentRuntime { this.atprotoObjectContext, ); } + if (declaration.contextStrategy === "coil-public-knowledge") { + if (!this.publicKnowledgeContext) { + throw new Error("Coil Public Knowledge context is not configured for this consumer process"); + } + return buildPublicKnowledgeContextPacket( + this.store, + declaration, + event, + this.publicKnowledgeContext, + ); + } if (declaration.contextStrategy === "telegram-conversation") { return declaration.contextDocumentSubscriptions?.length ? buildSubscribedTelegramConversationContextPacket(declaration, event, this.store) @@ -756,6 +784,59 @@ export class ThoughtAgentRuntime { return buildRepairContextPacket(declaration, event, originalSource, originalDeclaration); } + private async settlePreflightSkip( + declaration: ThoughtAgentDeclaration, + event: ThoughtEvent, + execution: string, + runId: string, + attempt: number, + skip: PublicKnowledgeEligibilitySkip, + ): Promise { + const at = new Date().toISOString(); + const run: AgentRun = { + id: runId, + executionKey: execution, + triggerEventId: event.id, + agentId: declaration.id, + agentVersion: declaration.version, + status: "skipped", + inputEventIds: [event.id], + outputEventIds: [], + attempt, + provider: declaration.provider ?? declaration.mode, + model: declaration.model ?? declaration.mode, + privacy: outputPrivacy(declaration, event), + adapterRevision: executionAdapterRevisionFor(declaration), + executionAdapterRevision: executionAdapterRevisionFor(declaration), + promptHash: sha256(declaration.systemPrompt), + contextManifest: { + inputEventIds: [event.id], + includedEventIds: [], + omittedEventIds: [event.id], + skip: { code: skip.code, evidence: skip.evidence }, + privacy: executionPrivacy(declaration, event), + ...adapterEvidence(declaration), + }, + errorText: skip.message, + createdAt: at, + startedAt: at, + completedAt: at, + updatedAt: at, + }; + await this.store.upsertRun(run); + await this.store.settleConsumerFailure({ + run, + inputEvent: event, + terminal: this.lifecycleCandidate("skipped", declaration, event, run, at, { + status: "skipped", + reasonCode: skip.code, + evidence: skip.evidence, + }), + progress: progressFor(declaration, event, at), + }); + return { declaration, runId, error: skip.message }; + } + private async consumerProgressForStart( declaration: ThoughtAgentDeclaration, source: string, @@ -1035,7 +1116,7 @@ export class ThoughtAgentRuntime { } private lifecycleCandidate( - phase: "started" | "trace" | "completed" | "failed" | "blocked" | "abandoned", + phase: "started" | "trace" | "completed" | "failed" | "blocked" | "skipped" | "abandoned", declaration: ThoughtAgentDeclaration, event: ThoughtEvent, run: AgentRun, @@ -1140,11 +1221,19 @@ function assertOutputContractEventBinding(declaration: ThoughtAgentDeclaration): if ((declaration.role ?? "standard") === "standard" && reviewResponseOutput !== reviewResponseEvent) { throw new Error(`Agent ${declaration.id}@${declaration.version} must bind review response output and review response events together`); } + const publicKnowledgeOutput = contractId === PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT_ID; + const publicKnowledgeEvent = declaration.outputEventType === "stream.thought.derived.public-knowledge.recommendation"; + if ((declaration.role ?? "standard") === "standard" && publicKnowledgeOutput !== publicKnowledgeEvent) { + throw new Error(`Agent ${declaration.id}@${declaration.version} must bind Public Knowledge recommendation output and events together`); + } const conversationText = (declaration.mode === "pi" && declaration.outputMode === "conversation-text") || (declaration.mode === "letta-agent-sdk" && declaration.lettaAgent?.responseMode === "conversation-text"); if (conceptualizationOutput && conversationText) { throw new Error(`Agent ${declaration.id}@${declaration.version} requires strict JSON for conceptualization output`); } + if (publicKnowledgeOutput && conversationText) { + throw new Error(`Agent ${declaration.id}@${declaration.version} requires strict JSON for Public Knowledge recommendations`); + } if (conversationText && contractId !== OBSERVATION_OUTPUT_CONTRACT_ID) { throw new Error(`Agent ${declaration.id}@${declaration.version} requires the observation contract for conversation-text output`); } diff --git a/src/agents/types.ts b/src/agents/types.ts index 95e5b64..07ea116 100644 --- a/src/agents/types.ts +++ b/src/agents/types.ts @@ -22,13 +22,15 @@ export interface AgentRetryPolicy { } export interface LettaAgentSdkConfiguration { - backend: "cloud"; + backend: "cloud" | "local"; agentIdEnv: string; agentId?: string | undefined; - conversation: "main"; + conversation: "main" | "per-document"; responseMode: "strict-json" | "conversation-text"; - permissionMode: "standard" | "acceptEdits" | "unrestricted"; + permissionMode: "standard" | "acceptEdits" | "unrestricted" | "strict"; skillSources?: LettaSkillSource[] | undefined; + memoryDirEnv?: string | undefined; + reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | undefined; dreaming: { trigger: "off" | "step-count" | "compaction-event"; stepCount?: number | undefined; @@ -48,7 +50,7 @@ export interface ThoughtAgentDeclaration { role?: "standard" | "repair"; outputContract?: OutputContractIdentity; declarationFingerprint?: string; - provider?: "tinker" | "openai-compatible" | "letta-cloud" | undefined; + provider?: "tinker" | "openai-compatible" | "letta-cloud" | "letta-local" | undefined; providerProfile?: string | undefined; modelTier?: "triage-small" | "reasoning-small" | "escalation" | undefined; model?: string | undefined; @@ -70,7 +72,7 @@ export interface ThoughtAgentDeclaration { enabled: boolean; maxEvents: number; maxInputChars: number; - contextStrategy?: "single-event" | "telegram-conversation" | "atproto-batch" | undefined; + contextStrategy?: "single-event" | "telegram-conversation" | "atproto-batch" | "coil-public-knowledge" | undefined; contextDocumentMaxChars?: number | undefined; contextDocumentSubscriptions?: ContextDocumentSubscription[] | undefined; conversationHistoryAgentIds?: string[] | undefined; diff --git a/src/cli.ts b/src/cli.ts index ba2e36e..5db9736 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -152,7 +152,7 @@ try { running = false; if (pending) schedule(); } - process.stdout.write(`thought stream watching read-only source ${source}: ${root}\n`); + process.stdout.write(`thought stream watching read-only source ${source}${producerOnly ? " in producer-only mode" : ""}: ${root}\n`); await waitForSignal(); process.stderr.write("thought stream watcher stopping.\n"); pending = false; @@ -789,9 +789,20 @@ async function createAgentRuntime(): Promise { const maxConcurrentOperations = override ? positiveInteger(override, "--max-concurrent-operations", 64) : configuredScheduler.maxConcurrentOperations; + const publicKnowledgePolicy = process.env.THOUGHTSTREAM_PUBLIC_KNOWLEDGE_POLICY_PATH?.trim(); + const publicKnowledgeCatalog = process.env.THOUGHTSTREAM_PUBLIC_KNOWLEDGE_CATALOG_ROOT?.trim(); + if (Boolean(publicKnowledgePolicy) !== Boolean(publicKnowledgeCatalog)) { + throw new Error("Public Knowledge runtime requires both policy and catalog-root environment paths"); + } return new ThoughtAgentRuntime(store, undefined, { maxConcurrentOperations, reconcileIntervalMs: configuredScheduler.reconcileIntervalMs, + ...(publicKnowledgePolicy && publicKnowledgeCatalog ? { + publicKnowledgeContext: { + policyPath: publicKnowledgePolicy, + catalogRoot: publicKnowledgeCatalog, + }, + } : {}), }); } diff --git a/src/events/registry.ts b/src/events/registry.ts index 04ee5e7..8996fbb 100644 --- a/src/events/registry.ts +++ b/src/events/registry.ts @@ -3,6 +3,8 @@ import { CONCEPTUALIZATION_OUTPUT_CONTRACT, conceptualizationOutputSchema, createOutputContractRegistry, + PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT, + publicKnowledgeRecommendationOutputSchema, } from "../agents/output-contracts.js"; import { sha256, type JsonObject, type JsonValue } from "../core/json.js"; import { modelAdapterIdentitySchema } from "../adapters/model-adapters.js"; @@ -340,6 +342,47 @@ const conceptualizationGraphPayload = z.object({ } }) as unknown as z.ZodType; +const publicKnowledgeRecommendationPayload = z.object({ + runId: z.string().min(1), + executionKey: z.string().min(1), + inputEventId: z.string().min(1), + inputSourceSequence: z.number().int().positive(), + summary: z.string().min(1).max(1_600), + confidence: z.enum(["low", "medium", "high"]), + outputContract: outputContractIdentitySchema, + structuredOutput: publicKnowledgeRecommendationOutputSchema, + model: objectPayload.optional(), + executionAdapterRevision: z.string().min(1).max(500).optional(), +}).strict().superRefine((value, context) => { + const expected = PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT.identity; + if (value.outputContract.id !== expected.id + || value.outputContract.version !== expected.version + || value.outputContract.sha256 !== expected.sha256) { + context.addIssue({ code: "custom", path: ["outputContract"], message: "Recommendation must name the canonical Public Knowledge contract" }); + } + if (value.summary !== value.structuredOutput.summary) { + context.addIssue({ code: "custom", path: ["summary"], message: "Summary must equal canonical structured output" }); + } + if (value.confidence !== value.structuredOutput.confidence) { + context.addIssue({ code: "custom", path: ["confidence"], message: "Confidence must equal canonical structured output" }); + } +}) as unknown as z.ZodType; + +const lettaConversationBindingPayload = z.object({ + id: z.string().min(1), + declarationId: z.string().min(1), + declarationVersion: z.number().int().positive(), + agentId: z.string().min(1), + source: z.string().min(1), + scopeType: z.literal("document"), + scopeKey: z.string().min(1), + conversationId: z.string().min(1), + remoteMarker: z.string().regex(/^thoughtstream:coil:[a-f0-9]{64}$/), + currentPath: z.string().min(1), + createdAt: z.iso.datetime(), + updatedAt: z.iso.datetime(), +}).strict() as unknown as z.ZodType; + const legacyJudgmentPayload = z.object({ runId: z.string().min(1), outputEventId: z.string().min(1), @@ -421,6 +464,7 @@ export function createDefaultRegistry(): EventRegistry { "stream.thought.agent.run.completed", "stream.thought.agent.run.failed", "stream.thought.agent.run.blocked", + "stream.thought.agent.run.skipped", "stream.thought.agent.run.abandoned", ]) { registry.register({ type, schemaVersion: 1, description: "Agent lifecycle evidence", payload: runPayload }); @@ -533,6 +577,22 @@ export function createDefaultRegistry(): EventRegistry { minimumPrivacy: "private", }); + registry.register({ + type: "stream.thought.derived.public-knowledge.recommendation", + schemaVersion: 1, + description: "Validated private editorial recommendation for one exact Coil document version", + payload: publicKnowledgeRecommendationPayload, + minimumPrivacy: "sensitive", + }); + + registry.register({ + type: "stream.thought.runtime.letta-conversation.binding", + schemaVersion: 1, + description: "Immutable evidence for one recoverable file-scoped Letta conversation binding", + payload: lettaConversationBindingPayload, + minimumPrivacy: "sensitive", + }); + registry.register({ type: OPERATIONAL_INCIDENT_EVENT_TYPE, schemaVersion: 1, diff --git a/src/jazz/store.ts b/src/jazz/store.ts index e098ad1..f009515 100644 --- a/src/jazz/store.ts +++ b/src/jazz/store.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { RowChangeKind, type Db, type TransactionScope } from "jazz-tools"; import { createJazzContext, type JazzContext } from "jazz-tools/backend"; import { canonicalJson, hashJson, parseJsonObject, sha256, type JsonObject, type JsonValue } from "../core/json.js"; +import { stableKey } from "../core/ids.js"; import { modelAdapterIdentitySchema, type ModelAdapterIdentity } from "../adapters/model-adapters.js"; import { createDefaultRegistry, type EventRegistry } from "../events/registry.js"; import type { @@ -28,6 +29,7 @@ import type { InferenceReservationEstimate, InferenceReservationRequest, InferenceUsage, + LettaConversationBinding, ProducerBatchResult, Projection, SourceState, @@ -661,6 +663,82 @@ export class JazzThoughtStore { || left.source.localeCompare(right.source)); } + async getLettaConversationBinding(id: string): Promise { + const events = await this.listLettaConversationBindingEvents(); + const event = [...events].reverse().find((candidate) => candidate.payload.id === id); + if (event) { + const binding = lettaConversationBindingFromPayload(event.payload); + const projectionId = lettaConversationProjectionId(id); + const projection = await this.getProjection(projectionId); + if (!projection || projection.lastEventId !== event.id) { + await this.upsertProjection(lettaConversationProjection(binding, event.id)); + } + return binding; + } + const projection = await this.getProjection(lettaConversationProjectionId(id)); + return projection ? lettaConversationBindingFromPayload(projection.payload) : undefined; + } + + async listLettaConversationBindings(): Promise { + const latest = new Map(); + for (const event of await this.listLettaConversationBindingEvents()) { + const binding = lettaConversationBindingFromPayload(event.payload); + latest.set(binding.id, { binding, eventId: event.id }); + } + return [...latest.values()].map((value) => value.binding).sort((left, right) => left.id.localeCompare(right.id)); + } + + async upsertLettaConversationBinding(binding: LettaConversationBinding): Promise { + const bindings = await this.listLettaConversationBindings(); + const existing = bindings.find((candidate) => candidate.id === binding.id); + if (bindings.some((candidate) => candidate.remoteMarker === binding.remoteMarker && candidate.id !== binding.id)) { + throw new Error(`Letta conversation marker is already bound to another scope: ${binding.remoteMarker}`); + } + const next = existing ? { + ...existing, + declarationVersion: binding.declarationVersion, + currentPath: binding.currentPath, + updatedAt: binding.updatedAt, + } : binding; + if (existing) { + assertSameLettaConversationBinding(existing, binding); + if (binding.declarationVersion < existing.declarationVersion) { + throw new Error(`Letta conversation declaration version regression for ${binding.id}`); + } + if (binding.declarationVersion === existing.declarationVersion && binding.currentPath === existing.currentPath) { + return existing; + } + } + const appended = await this.appendEvent({ + type: "stream.thought.runtime.letta-conversation.binding", + schemaVersion: 1, + source: "runtime:letta-conversations", + sourceKind: "system", + externalId: next.id, + idempotencyKey: [ + next.id, + String(next.declarationVersion), + sha256(next.currentPath), + sha256(next.updatedAt), + next.remoteMarker, + ].join(":"), + occurredAt: next.updatedAt, + actor: "thoughtstream", + correlationId: next.id, + privacy: "sensitive", + payload: lettaConversationBindingPayload(next), + }); + await this.upsertProjection(lettaConversationProjection(next, appended.event.id)); + return next; + } + + private async listLettaConversationBindingEvents(): Promise { + return this.listEvents({ + source: "runtime:letta-conversations", + types: ["stream.thought.runtime.letta-conversation.binding"], + }); + } + subscribeSources(onSources: (sources: SourceState[]) => void): () => void { return this.db.subscribeAll( thoughtstreamApp.sources.where({ enabled: true }), @@ -1553,6 +1631,83 @@ function consumerProgressFromJazz(row: Record): ConsumerProgres }; } +function lettaConversationBindingFromPayload(payload: JsonObject): LettaConversationBinding { + const scopeType = String(payload.scopeType); + if (scopeType !== "document") throw new Error(`Unsupported Letta conversation scope: ${scopeType}`); + const binding: LettaConversationBinding = { + id: String(payload.id), + declarationId: String(payload.declarationId), + declarationVersion: Number(payload.declarationVersion), + agentId: String(payload.agentId), + source: String(payload.source), + scopeType, + scopeKey: String(payload.scopeKey), + conversationId: String(payload.conversationId), + remoteMarker: String(payload.remoteMarker), + currentPath: String(payload.currentPath), + createdAt: String(payload.createdAt), + updatedAt: String(payload.updatedAt), + }; + if (!binding.id || !binding.declarationId || !Number.isSafeInteger(binding.declarationVersion) + || binding.declarationVersion <= 0 || !binding.agentId || !binding.source || !binding.scopeKey + || !binding.conversationId || !binding.remoteMarker || !binding.currentPath + || !Number.isFinite(Date.parse(binding.createdAt)) || !Number.isFinite(Date.parse(binding.updatedAt))) { + throw new Error("Letta conversation binding payload is invalid"); + } + return binding; +} + +function lettaConversationBindingPayload(binding: LettaConversationBinding): JsonObject { + return { + id: binding.id, + declarationId: binding.declarationId, + declarationVersion: binding.declarationVersion, + agentId: binding.agentId, + source: binding.source, + scopeType: binding.scopeType, + scopeKey: binding.scopeKey, + conversationId: binding.conversationId, + remoteMarker: binding.remoteMarker, + currentPath: binding.currentPath, + createdAt: binding.createdAt, + updatedAt: binding.updatedAt, + }; +} + +function lettaConversationProjectionId(bindingId: string): string { + return stableKey("projection", "letta-conversation-binding", bindingId); +} + +function lettaConversationProjection(binding: LettaConversationBinding, lastEventId: string): Projection { + return { + id: lettaConversationProjectionId(binding.id), + payload: lettaConversationBindingPayload(binding), + lastEventId, + projectionVersion: 1, + updatedAt: binding.updatedAt, + }; +} + +function assertSameLettaConversationBinding( + existing: LettaConversationBinding, + candidate: LettaConversationBinding, +): void { + for (const key of [ + "id", + "declarationId", + "agentId", + "source", + "scopeType", + "scopeKey", + "conversationId", + "remoteMarker", + ] as const) { + if (existing[key] !== candidate[key]) { + throw new Error(`Letta conversation binding conflict for ${existing.id}: ${key}`); + } + } +} + function cursorToJazz(cursor: SourceCursor): Record { return { key: cursor.id, diff --git a/src/public-knowledge/context.ts b/src/public-knowledge/context.ts new file mode 100644 index 0000000..f109584 --- /dev/null +++ b/src/public-knowledge/context.ts @@ -0,0 +1,344 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import fg from "fast-glob"; +import YAML from "yaml"; +import { z } from "zod"; +import { canonicalJson, sha256, type JsonObject } from "../core/json.js"; +import type { ThoughtEvent } from "../events/types.js"; +import type { JazzThoughtStore } from "../jazz/store.js"; +import type { AgentContextPacket } from "../agents/context.js"; +import { declarationFingerprint } from "../agents/declarations.js"; +import { + outputContractForDeclaration, + outputContractIdentityJson, + type PublicKnowledgeRecommendationOutput, +} from "../agents/output-contracts.js"; +import type { ThoughtAgentDeclaration } from "../agents/types.js"; + +const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/); +const policySchema = z.object({ + version: z.number().int().positive(), + defaultDecision: z.string().min(1).default("deny"), + blockedPrefixes: z.array(z.string().min(1)).default([]), + blockedWikilinkPrefixes: z.array(z.string().min(1)).default([]), + sourceMappings: z.array(z.object({ + pattern: z.string().min(1), + decision: z.string().min(1), + }).passthrough()).default([]), +}).passthrough(); + +export interface PublicKnowledgeContextOptions { + policyPath: string; + catalogRoot: string; + maxCatalogEntries?: number | undefined; + maxSourceChars?: number | undefined; +} + +export interface PublicKnowledgeContextEvidence { + documentId: string; + versionId: string; + path: string; + sha256: string; + policySha256: string; + catalogSha256: string; + catalogSlugs: string[]; + blockedWikilinkCount: number; + blockedWikilinksSha256?: string | undefined; +} + +export class PublicKnowledgeEligibilitySkip extends Error { + constructor( + readonly code: + | "deleted" + | "blocked-path" + | "deferred-path" + | "unsupported-content" + | "source-too-large", + readonly evidence: JsonObject, + ) { + super(`Public Knowledge input skipped: ${code}`); + this.name = "PublicKnowledgeEligibilitySkip"; + } +} + +export async function buildPublicKnowledgeContextPacket( + store: JazzThoughtStore, + declaration: ThoughtAgentDeclaration, + event: ThoughtEvent, + options: PublicKnowledgeContextOptions, +): Promise { + assertFileEvent(event); + const documentId = requiredString(event.payload.documentId, "documentId"); + const currentPath = requiredString(event.payload.path, "path"); + if (event.type === "stream.thought.source.file.deleted") { + throw new PublicKnowledgeEligibilitySkip("deleted", { documentId, pathSha256: sha256(currentPath) }); + } + const versionId = requiredString(event.payload.versionId, "versionId"); + const sourceSha256 = requiredSha256(event.payload.sha256, "sha256"); + const loadedPolicy = await loadPolicy(options.policyPath); + const classification = classifyPath(currentPath, loadedPolicy.policy); + const baseEvidence = { + documentId, + versionId, + pathSha256: sha256(currentPath), + sourceSha256, + policySha256: loadedPolicy.sha256, + }; + if (classification === "blocked") { + throw new PublicKnowledgeEligibilitySkip("blocked-path", baseEvidence); + } + if (classification !== "eligible") { + throw new PublicKnowledgeEligibilitySkip("deferred-path", baseEvidence); + } + const version = await store.getDocumentVersion(versionId); + if (!version + || version.source !== event.source + || version.documentId !== documentId + || version.path !== currentPath + || version.sha256 !== sourceSha256 + || sha256(version.content) !== sourceSha256) { + throw new Error("Public Knowledge document-version evidence is missing or inconsistent"); + } + if (version.contentType !== "text/markdown" && version.contentType !== "text/mdx") { + throw new PublicKnowledgeEligibilitySkip("unsupported-content", { + ...baseEvidence, + contentType: version.contentType, + }); + } + const maxSourceChars = options.maxSourceChars ?? 64_000; + if (version.content.length > maxSourceChars) { + throw new PublicKnowledgeEligibilitySkip("source-too-large", { + ...baseEvidence, + sourceChars: version.content.length, + maximumChars: maxSourceChars, + }); + } + + const catalog = await loadPublicCatalog(options.catalogRoot, options.maxCatalogEntries ?? 2_000); + const blockedWikilinks = findBlockedWikilinks(version.content, loadedPolicy.policy.blockedWikilinkPrefixes); + const evidence: PublicKnowledgeContextEvidence = { + documentId, + versionId, + path: currentPath, + sha256: sourceSha256, + policySha256: loadedPolicy.sha256, + catalogSha256: catalog.sha256, + catalogSlugs: catalog.entries.map((entry) => entry.slug), + blockedWikilinkCount: blockedWikilinks.length, + ...(blockedWikilinks.length > 0 + ? { blockedWikilinksSha256: sha256(canonicalJson(blockedWikilinks)) } + : {}), + }; + const envelope = { + purpose: "private Public Knowledge recommendation only", + source: { documentId, versionId, path: currentPath, sha256: sourceSha256 }, + policy: { + sha256: loadedPolicy.sha256, + blockedWikilinkCount: blockedWikilinks.length, + blockedWikilinks, + }, + publicCatalog: catalog.entries, + authority: { + mayRecommend: true, + mayEditCoil: false, + mayStage: false, + mayPublish: false, + mayDeploy: false, + }, + }; + const text = [ + "The following Coil document is private source evidence. Recommend only material suitable for the reviewed Public Knowledge pipeline.", + "Do not expose personal context, infer publication authorization, or treat this packet as permission to edit or publish.", + `${canonicalJson(envelope)}`, + `${version.content}`, + ].join("\n\n"); + if (text.length > declaration.maxInputChars) { + throw new PublicKnowledgeEligibilitySkip("source-too-large", { + ...baseEvidence, + compiledChars: text.length, + maximumChars: declaration.maxInputChars, + }); + } + return { + text, + manifest: { + inputEventIds: [event.id], + includedEventIds: [event.id], + omittedEventIds: [], + maxEvents: declaration.maxEvents, + maxChars: declaration.maxInputChars, + sourceOriginalChars: version.content.length, + sourceIncludedChars: version.content.length, + truncated: false, + promptRef: declaration.promptRef, + promptRevision: sha256(declaration.systemPrompt), + declarationFingerprint: declaration.declarationFingerprint ?? declarationFingerprint(declaration), + agentVersion: declaration.version, + agentRole: declaration.role ?? "standard", + outputContract: outputContractIdentityJson(outputContractForDeclaration(declaration)), + publicKnowledge: { + documentId, + versionId, + pathSha256: sha256(currentPath), + sourceSha256, + policySha256: loadedPolicy.sha256, + catalogSha256: catalog.sha256, + catalogSlugs: evidence.catalogSlugs, + blockedWikilinkCount: blockedWikilinks.length, + ...(evidence.blockedWikilinksSha256 + ? { blockedWikilinksSha256: evidence.blockedWikilinksSha256 } + : {}), + }, + tools: declaration.tools, + externalActions: declaration.externalActions, + }, + }; +} + +export function validatePublicKnowledgeRecommendationAgainstContext( + output: PublicKnowledgeRecommendationOutput, + context: AgentContextPacket, +): void { + const evidence = record(context.manifest.publicKnowledge); + if (!evidence) throw new Error("Public Knowledge context evidence is unavailable"); + const catalogSlugs = Array.isArray(evidence.catalogSlugs) + ? evidence.catalogSlugs.filter((value): value is string => typeof value === "string") + : []; + const known = new Set(catalogSlugs); + if (output.decision === "revise-existing") { + const unknown = output.targetSlugs.filter((slug) => !known.has(slug)); + if (unknown.length > 0) throw new Error(`Public Knowledge recommendation names unknown targets: ${unknown.join(", ")}`); + } + if (output.decision === "propose-new" && output.candidate && known.has(output.candidate.slug)) { + throw new Error("Public Knowledge proposal collides with an existing catalog slug"); + } + if (Number(evidence.blockedWikilinkCount ?? 0) > 0 && output.privacy.status === "clear") { + throw new Error("Public Knowledge recommendation cannot be privacy-clear with blocked wikilinks"); + } +} + +async function loadPolicy(policyPath: string): Promise<{ + policy: z.infer; + sha256: string; +}> { + const resolved = path.resolve(policyPath); + const stat = await fs.lstat(resolved); + if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("Public Knowledge policy must be a regular file"); + const raw = await fs.readFile(resolved, "utf8"); + return { policy: policySchema.parse(JSON.parse(raw)), sha256: sha256(raw) }; +} + +async function loadPublicCatalog(rootPath: string, maximum: number): Promise<{ + entries: Array<{ slug: string; title: string; summary: string }>; + sha256: string; +}> { + const root = path.resolve(rootPath); + const realRoot = await fs.realpath(root); + const rootStat = await fs.lstat(root); + if (!rootStat.isDirectory() || rootStat.isSymbolicLink() || root !== realRoot) { + throw new Error("Public Knowledge catalog root must be a real directory"); + } + const paths = await fg("**/*.md", { + cwd: root, + onlyFiles: true, + dot: false, + followSymbolicLinks: false, + unique: true, + }); + if (paths.length > maximum) throw new Error("Public Knowledge catalog exceeds the configured entry bound"); + const entries: Array<{ slug: string; title: string; summary: string }> = []; + for (const relative of paths.sort()) { + const absolute = path.resolve(root, relative); + const real = await fs.realpath(absolute); + if (!inside(root, real)) throw new Error("Public Knowledge catalog entry escaped its root"); + const stat = await fs.lstat(real); + if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("Public Knowledge catalog entry must be a regular file"); + const frontmatter = parseFrontmatter(await fs.readFile(real, "utf8")); + const slug = typeof frontmatter.slug === "string" && frontmatter.slug.trim() + ? frontmatter.slug.trim() + : path.basename(relative, path.extname(relative)); + if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) { + throw new Error(`Public Knowledge catalog slug is invalid: ${slug}`); + } + if (entries.some((entry) => entry.slug === slug)) { + throw new Error(`Public Knowledge catalog slug is duplicated: ${slug}`); + } + entries.push({ + slug, + title: typeof frontmatter.title === "string" ? frontmatter.title : "", + summary: typeof frontmatter.summary === "string" ? frontmatter.summary : "", + }); + } + return { entries, sha256: sha256(canonicalJson(entries)) }; +} + +function classifyPath(sourcePath: string, policy: z.infer): "eligible" | "blocked" | "deferred" { + const normalized = normalizePath(sourcePath); + if (policy.blockedPrefixes.some((prefix) => pathInPrefix(normalized, normalizePath(prefix)))) return "blocked"; + for (const rule of policy.sourceMappings) { + if (matchesGlob(normalized, normalizePath(rule.pattern))) { + return rule.decision === "stage" ? "eligible" : rule.decision === "deny" ? "blocked" : "deferred"; + } + } + return policy.defaultDecision === "deny" ? "deferred" : "deferred"; +} + +function findBlockedWikilinks(content: string, prefixes: string[]): string[] { + const findings = new Set(); + for (const match of content.matchAll(/\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|[^\]]*)?\]\]/g)) { + const target = normalizePath(match[1]!.trim()); + if (prefixes.some((prefix) => pathInPrefix(target, normalizePath(prefix)))) findings.add(target); + } + return [...findings].sort(); +} + +function parseFrontmatter(content: string): Record { + if (!content.startsWith("---\n")) return {}; + const end = content.indexOf("\n---\n", 4); + if (end < 0) return {}; + const parsed: unknown = YAML.parse(content.slice(4, end)); + return record(parsed) ?? {}; +} + +function assertFileEvent(event: ThoughtEvent): void { + if (!["stream.thought.source.file.added", "stream.thought.source.file.changed", "stream.thought.source.file.renamed", "stream.thought.source.file.deleted"].includes(event.type)) { + throw new Error("Public Knowledge context requires a filesystem document event"); + } +} + +function requiredString(value: unknown, field: string): string { + if (typeof value !== "string" || !value) throw new Error(`Public Knowledge event ${field} is invalid`); + return value; +} + +function requiredSha256(value: unknown, field: string): string { + const parsed = sha256Schema.safeParse(value); + if (!parsed.success) throw new Error(`Public Knowledge event ${field} is invalid`); + return parsed.data; +} + +function matchesGlob(value: string, pattern: string): boolean { + const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&"); + const expression = escaped.replaceAll("**", "\u0000").replaceAll("*", "[^/]*").replaceAll("\u0000", ".*"); + return new RegExp(`^${expression}$`).test(value); +} + +function pathInPrefix(value: string, prefix: string): boolean { + const normalized = prefix.replace(/\/$/, ""); + return value === normalized || value.startsWith(`${normalized}/`); +} + +function normalizePath(value: string): string { + return value.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, ""); +} + +function inside(root: string, candidate: string): boolean { + const relative = path.relative(root, candidate); + return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); +} + +function record(value: unknown): Record | undefined { + return value && typeof value === "object" && !Array.isArray(value) + ? value as Record + : undefined; +} diff --git a/src/runtime/credential-compartments.ts b/src/runtime/credential-compartments.ts index f726b2a..92a28ba 100644 --- a/src/runtime/credential-compartments.ts +++ b/src/runtime/credential-compartments.ts @@ -26,6 +26,12 @@ const outputNames: Record = { jetstream: "jetstream.env", }; +const consumerRuntimeVariables = new Set([ + "THOUGHTSTREAM_ENABLE_COIL_PUBLIC_KNOWLEDGE", + "THOUGHTSTREAM_PUBLIC_KNOWLEDGE_POLICY_PATH", + "THOUGHTSTREAM_PUBLIC_KNOWLEDGE_CATALOG_ROOT", +]); + export async function splitServiceCredentialFile( source: string, outputDirectory: string, @@ -164,7 +170,8 @@ function isTelegramBotToken(name: string): boolean { } function isConsumerVariable(name: string, providers: ConsumerCredentialProvider[]): boolean { - return (providers.includes("letta") && (name === "LETTA_API_KEY" || /^THOUGHTSTREAM_LETTA_[A-Z0-9_]+$/.test(name))) + return consumerRuntimeVariables.has(name) + || (providers.includes("letta") && (name === "LETTA_API_KEY" || /^THOUGHTSTREAM_LETTA_[A-Z0-9_]+$/.test(name))) || (providers.includes("tinker") && (name === "TINKER_API_KEY" || /^THOUGHTSTREAM_TINKER_[A-Z0-9_]+$/.test(name))) || (providers.includes("openai") && name === "OPENAI_API_KEY") || (providers.includes("openai-compatible") && (name === "THOUGHTSTREAM_MODEL_API_KEY" || /^THOUGHTSTREAM_MODEL_[A-Z0-9_]+$/.test(name))); diff --git a/src/store/types.ts b/src/store/types.ts index b66d298..5bb2e96 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -14,7 +14,22 @@ export interface StoredAgent { updatedAt: string; } -export type AgentRunStatus = "running" | "completed" | "failed" | "blocked" | "abandoned"; +export type AgentRunStatus = "running" | "completed" | "failed" | "blocked" | "skipped" | "abandoned"; + +export interface LettaConversationBinding { + id: string; + declarationId: string; + declarationVersion: number; + agentId: string; + source: string; + scopeType: "document"; + scopeKey: string; + conversationId: string; + remoteMarker: string; + currentPath: string; + createdAt: string; + updatedAt: string; +} export type InferenceBudgetScopeType = "agent" | "provider" | "global"; export type InferenceBudgetWindowKind = "rolling" | "hour" | "day"; diff --git a/test/credential-compartments.test.ts b/test/credential-compartments.test.ts index 4569727..b684b96 100644 --- a/test/credential-compartments.test.ts +++ b/test/credential-compartments.test.ts @@ -23,6 +23,10 @@ describe("service credential compartments", () => { letta: generatedValue("letta"), tinker: generatedValue("tinker"), agent: generatedValue("agent"), + coAgent: generatedValue("co-agent"), + memory: generatedValue("memory-root"), + policy: generatedValue("policy-path"), + catalog: generatedValue("catalog-root"), bot: generatedValue("bot"), webhook: generatedValue("webhook"), }; @@ -31,6 +35,11 @@ describe("service credential compartments", () => { `LETTA_API_KEY=${values.letta}`, `TINKER_API_KEY=${values.tinker}`, `THOUGHTSTREAM_LETTA_TELEGRAM_AGENT_ID=${values.agent}`, + `THOUGHTSTREAM_LETTA_CO_AGENT_ID=${values.coAgent}`, + `THOUGHTSTREAM_LETTA_CO_MEMORY_DIR=${values.memory}`, + "THOUGHTSTREAM_ENABLE_COIL_PUBLIC_KNOWLEDGE=1", + `THOUGHTSTREAM_PUBLIC_KNOWLEDGE_POLICY_PATH=${values.policy}`, + `THOUGHTSTREAM_PUBLIC_KNOWLEDGE_CATALOG_ROOT=${values.catalog}`, `THOUGHTSTREAM_TELEGRAM_BOT_TOKEN=${values.bot}`, `THOUGHTSTREAM_TELEGRAM_WEBHOOK_SECRET=${values.webhook}`, "THOUGHTSTREAM_TELEGRAM_API_BASE_URL=https://telegram.example.invalid", @@ -56,7 +65,12 @@ describe("service credential compartments", () => { ]); expect(receipt.files.consumer.variableNames).toEqual([ "LETTA_API_KEY", + "THOUGHTSTREAM_ENABLE_COIL_PUBLIC_KNOWLEDGE", + "THOUGHTSTREAM_LETTA_CO_AGENT_ID", + "THOUGHTSTREAM_LETTA_CO_MEMORY_DIR", "THOUGHTSTREAM_LETTA_TELEGRAM_AGENT_ID", + "THOUGHTSTREAM_PUBLIC_KNOWLEDGE_CATALOG_ROOT", + "THOUGHTSTREAM_PUBLIC_KNOWLEDGE_POLICY_PATH", ]); expect(receipt.files.jetstream.variableNames).toEqual(["THOUGHTSTREAM_JETSTREAM_URL"]); expect(receipt.omittedVariableNames).toEqual([ @@ -79,6 +93,10 @@ describe("service credential compartments", () => { expect(dispatcherText).not.toContain(values.letta); expect(consumerText).toContain(values.letta); expect(consumerText).toContain(values.agent); + expect(consumerText).toContain(values.coAgent); + expect(consumerText).toContain(values.memory); + expect(consumerText).toContain(values.policy); + expect(consumerText).toContain(values.catalog); expect(consumerText).not.toContain(values.bot); expect(consumerText).not.toContain(values.tinker); for (const value of Object.values(values)) expect(jetstreamText).not.toContain(value); diff --git a/test/declarations.test.ts b/test/declarations.test.ts index 5a99c24..8b21766 100644 --- a/test/declarations.test.ts +++ b/test/declarations.test.ts @@ -208,6 +208,49 @@ describe("agent declarations", () => { }); }); + test("activates the disabled local Coil consumer only through its explicit environment gate", async () => { + const project = await temporaryProject(); + roots.push(project); + const agents = path.join(project, "agents"); + const prompts = path.join(project, "prompts"); + await fs.mkdir(agents, { recursive: true }); + await fs.mkdir(prompts, { recursive: true }); + await fs.copyFile( + path.join(process.cwd(), "agents", "coil-public-knowledge.yaml"), + path.join(agents, "coil-public-knowledge.yaml"), + ); + await fs.copyFile( + path.join(process.cwd(), "prompts", "coil-public-knowledge.md"), + path.join(prompts, "coil-public-knowledge.md"), + ); + + const [inactive] = await loadAgentDeclarations(agents, {}); + expect(inactive?.enabled).toBe(false); + expect(inactive?.lettaAgent?.agentId).toBeUndefined(); + const [active] = await loadAgentDeclarations(agents, { + THOUGHTSTREAM_ENABLE_COIL_PUBLIC_KNOWLEDGE: "1", + THOUGHTSTREAM_LETTA_CO_AGENT_ID: "agent-co-fixture", + THOUGHTSTREAM_LETTA_CO_MEMORY_DIR: "/tmp/co-memory", + }); + expect(active).toMatchObject({ + id: "coil-public-knowledge", + version: 2, + enabled: true, + provider: "letta-local", + contextStrategy: "coil-public-knowledge", + lettaAgent: { + backend: "local", + agentId: "agent-co-fixture", + conversation: "per-document", + permissionMode: "strict", + skillSources: [], + }, + }); + await expect(loadAgentDeclarations(agents, { + THOUGHTSTREAM_ENABLE_COIL_PUBLIC_KNOWLEDGE: "sometimes", + })).rejects.toThrow("must be one of 1, true, 0, or false"); + }); + test("rejects a cost limit when the resident declaration omits a cost reservation", async () => { const project = await temporaryProject(); roots.push(project); diff --git a/test/letta-agent-sdk.test.ts b/test/letta-agent-sdk.test.ts index b0694b6..4a83d09 100644 --- a/test/letta-agent-sdk.test.ts +++ b/test/letta-agent-sdk.test.ts @@ -611,7 +611,7 @@ class FakeSession { async listMessages(_options?: ListMessagesOptions): Promise { if (this.listMessagesError) throw this.listMessagesError; return { - messages: this.history, + messages: this.history as ListMessagesResult["messages"], nextBefore: this.historyNextBefore, hasMore: this.historyHasMore, }; diff --git a/test/public-knowledge-consumer.test.ts b/test/public-knowledge-consumer.test.ts new file mode 100644 index 0000000..07f9b1c --- /dev/null +++ b/test/public-knowledge-consumer.test.ts @@ -0,0 +1,454 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import type { + LettaCodeClientSessionOptions, + LettaCodeSession, + LettaConversation, + ListMessagesResult, + SDKMessage, + SendMessage, +} from "@letta-ai/letta-agent-sdk"; +import { afterEach, describe, expect, test } from "vitest"; +import { FilesystemConnector } from "../src/connectors/filesystem.js"; +import { + LettaAgentSdkRunner, + lettaDocumentConversationBindingId, + lettaDocumentConversationRemoteMarker, + type LettaAgentSdkClient, +} from "../src/agents/letta-agent-sdk.js"; +import { + PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT, + type PublicKnowledgeRecommendationOutput, +} from "../src/agents/output-contracts.js"; +import { ThoughtAgentRuntime } from "../src/agents/runtime.js"; +import type { + AgentOutput, + AgentRunInput, + AgentRunner, + RunnerTrace, + ThoughtAgentDeclaration, +} from "../src/agents/types.js"; +import { + buildPublicKnowledgeContextPacket, + PublicKnowledgeEligibilitySkip, + validatePublicKnowledgeRecommendationAgainstContext, +} from "../src/public-knowledge/context.js"; +import type { ThoughtEvent } from "../src/events/types.js"; +import type { JazzThoughtStore } from "../src/jazz/store.js"; +import { temporaryProject, testStore } from "./helpers.js"; + +const stores: JazzThoughtStore[] = []; +const roots: string[] = []; + +afterEach(async () => { + await Promise.all(stores.splice(0).map((store) => store.close())); + await Promise.all(roots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true }))); +}); + +describe("Coil Public Knowledge consumer", () => { + test("loads one exact eligible document and public frontmatter while blocking journal content before inference", async () => { + const fixture = await fixtureWorkspace(); + const declaration = publicKnowledgeDeclaration(); + const scan = await fixture.connector.scan(fixture.store); + const lesson = scan.events.find((event) => event.payload.path === "lessons/eligible.md")!; + const journal = scan.events.find((event) => event.payload.path === "journal/private.md")!; + + const packet = await buildPublicKnowledgeContextPacket(fixture.store, declaration, lesson, fixture.contextOptions); + + expect(packet.text).toContain("# Eligible source"); + expect(packet.text).toContain("agent-memory"); + expect(packet.text).not.toContain("PUBLIC BODY MUST NOT ENTER CATALOG CONTEXT"); + expect(packet.manifest).toMatchObject({ + truncated: false, + publicKnowledge: { + documentId: lesson.payload.documentId, + versionId: lesson.payload.versionId, + sourceSha256: lesson.payload.sha256, + catalogSlugs: ["agent-memory"], + }, + }); + await expect(buildPublicKnowledgeContextPacket( + fixture.store, + declaration, + journal, + fixture.contextOptions, + )).rejects.toMatchObject({ code: "blocked-path" } satisfies Partial); + let blockedContentReads = 0; + const contentTrap = { + getDocumentVersion: async () => { + blockedContentReads += 1; + throw new Error("blocked content must not be read"); + }, + } as unknown as JazzThoughtStore; + await expect(buildPublicKnowledgeContextPacket( + contentTrap, + declaration, + journal, + fixture.contextOptions, + )).rejects.toMatchObject({ code: "blocked-path" }); + expect(blockedContentReads).toBe(0); + }); + + test("settles blocked files as skipped before runner calls or inference accounting", async () => { + const fixture = await fixtureWorkspace(); + const scan = await fixture.connector.scan(fixture.store); + const declaration = publicKnowledgeDeclaration(); + const runner = new RecommendationProbe(); + const runtime = new ThoughtAgentRuntime(fixture.store, [runner], { + publicKnowledgeContext: fixture.contextOptions, + }); + + const results = await runtime.consumeBacklog([declaration]); + + expect(results).toHaveLength(scan.events.length); + expect(runner.calls).toBe(1); + const runs = await fixture.store.listRuns(); + expect(runs.map((run) => run.status).sort()).toEqual(["completed", "skipped"]); + const skipped = runs.find((run) => run.status === "skipped")!; + expect(skipped.contextManifest).toMatchObject({ skip: { code: "blocked-path" } }); + expect((await fixture.store.listInferenceAccounting({ agentId: declaration.id }))).toHaveLength(1); + expect((await fixture.store.listEvents({ types: ["stream.thought.agent.run.skipped"] }))).toHaveLength(1); + expect((await fixture.store.listEvents({ + types: ["stream.thought.derived.public-knowledge.recommendation"], + }))).toEqual([expect.objectContaining({ privacy: "sensitive" })]); + }); + + test("binds one local confined Co conversation to a stable document across path changes", async () => { + const fixture = await fixtureWorkspace(); + const [event] = (await fixture.connector.scan(fixture.store)).events.filter((candidate) => ( + candidate.payload.path === "lessons/eligible.md" + )); + const declaration = publicKnowledgeDeclaration(); + const context = await buildPublicKnowledgeContextPacket(fixture.store, declaration, event!, fixture.contextOptions); + const client = new FakeConversationClient(validRecommendation()); + const runner = new LettaAgentSdkRunner({ + client, + conversationStore: fixture.store, + environment: { THOUGHTSTREAM_LETTA_CO_MEMORY_DIR: "/tmp/co-memory" }, + reconciliationDelayMs: 0, + }); + + const first = await runner.run({ runId: "thought-run-first", declaration, event: event!, context }, async () => {}); + const renamed: ThoughtEvent = { + ...event!, + id: "event-renamed-fixture", + sourceSequence: event!.sourceSequence + 1, + type: "stream.thought.source.file.renamed", + payload: { ...event!.payload, path: "lessons/renamed.md", previousPath: "lessons/eligible.md" }, + }; + await runner.run({ runId: "thought-run-second", declaration, event: renamed, context }, async () => {}); + const upgradedDeclaration: ThoughtAgentDeclaration = { ...declaration, version: declaration.version + 1 }; + const upgradedEvent: ThoughtEvent = { + ...renamed, + id: "event-upgraded-declaration-fixture", + sourceSequence: renamed.sourceSequence + 1, + }; + await runner.run({ + runId: "thought-run-third", + declaration: upgradedDeclaration, + event: upgradedEvent, + context, + }, async () => {}); + + expect(first).toMatchObject({ decision: "revise-existing", targetSlugs: ["agent-memory"] }); + expect(client.created).toHaveLength(1); + expect(client.resumedIds).toEqual([client.created[0]!.id, client.created[0]!.id, client.created[0]!.id]); + expect(client.sessionOptions[0]).toMatchObject({ + permissionMode: "strict", + skillSources: [], + allowedTools: [], + cwd: "/tmp/co-memory", + env: { MEMORY_DIR: "/tmp/co-memory" }, + filesystemConfinement: "memory", + model: "chatgpt-plus-pro/gpt-5.6-luna", + reasoningEffort: "low", + }); + const [binding] = await fixture.store.listLettaConversationBindings(); + expect(binding).toMatchObject({ + agentId: "agent-co-fixture", + source: "filesystem:coil", + scopeType: "document", + scopeKey: event!.payload.documentId, + conversationId: client.created[0]!.id, + currentPath: "lessons/renamed.md", + declarationVersion: upgradedDeclaration.version, + }); + expect(binding!.remoteMarker).toBe(lettaDocumentConversationRemoteMarker( + lettaDocumentConversationBindingId(declaration, event!), + )); + }); + + test("recovers a summary-marked remote conversation and fails closed on duplicate markers", async () => { + const fixture = await fixtureWorkspace(); + const event = (await fixture.connector.scan(fixture.store)).events.find((candidate) => ( + candidate.payload.path === "lessons/eligible.md" + ))!; + const declaration = publicKnowledgeDeclaration(); + const context = await buildPublicKnowledgeContextPacket(fixture.store, declaration, event, fixture.contextOptions); + const marker = lettaDocumentConversationRemoteMarker(lettaDocumentConversationBindingId(declaration, event)); + const existing = conversation("conv-recovered", marker); + const client = new FakeConversationClient(validRecommendation(), [existing]); + const runner = new LettaAgentSdkRunner({ + client, + conversationStore: fixture.store, + environment: { THOUGHTSTREAM_LETTA_CO_MEMORY_DIR: "/tmp/co-memory" }, + reconciliationDelayMs: 0, + }); + + await runner.run({ runId: "thought-run-recover", declaration, event, context }, async () => {}); + + expect(client.created).toHaveLength(0); + expect(client.resumedIds).toEqual([existing.id]); + expect((await fixture.store.listLettaConversationBindings())[0]?.conversationId).toBe(existing.id); + + const duplicateRoot = await temporaryProject("thoughtstream-pk-duplicate-"); + roots.push(duplicateRoot); + const duplicateStore = testStore(duplicateRoot); + stores.push(duplicateStore); + const duplicateClient = new FakeConversationClient(validRecommendation(), [ + conversation("conv-duplicate-a", marker), + conversation("conv-duplicate-b", marker), + ]); + const duplicateRunner = new LettaAgentSdkRunner({ + client: duplicateClient, + conversationStore: duplicateStore, + environment: { THOUGHTSTREAM_LETTA_CO_MEMORY_DIR: "/tmp/co-memory" }, + reconciliationDelayMs: 0, + }); + await expect(duplicateRunner.run({ + runId: "thought-run-duplicate", + declaration, + event, + context, + }, async () => {})).rejects.toMatchObject({ + diagnostic: { code: "letta-conversation-marker-ambiguous" }, + }); + expect(duplicateClient.resumedIds).toHaveLength(0); + }); + + test("rejects context-invalid catalog and privacy claims after schema validation", async () => { + const fixture = await fixtureWorkspace("# Eligible source\n\n[[journal/private]]\n"); + const event = (await fixture.connector.scan(fixture.store)).events.find((candidate) => ( + candidate.payload.path === "lessons/eligible.md" + ))!; + const context = await buildPublicKnowledgeContextPacket( + fixture.store, + publicKnowledgeDeclaration(), + event, + fixture.contextOptions, + ); + expect(() => validatePublicKnowledgeRecommendationAgainstContext({ + ...validRecommendation(), + targetSlugs: ["missing-entry"], + }, context)).toThrow("unknown targets"); + expect(() => validatePublicKnowledgeRecommendationAgainstContext({ + ...validRecommendation(), + privacy: { status: "clear", findings: [] }, + }, context)).toThrow("blocked wikilinks"); + }); +}); + +class RecommendationProbe implements AgentRunner { + readonly mode = "letta-agent-sdk" as const; + calls = 0; + async run(_input: AgentRunInput, _onTrace: (trace: RunnerTrace) => Promise): Promise { + this.calls += 1; + return validRecommendation(); + } +} + +class FakeConversationClient implements LettaAgentSdkClient { + readonly created: LettaConversation[] = []; + readonly resumedIds: string[] = []; + readonly sessionOptions: Array = []; + private readonly remote = new Map(); + private sequence = 0; + + readonly conversations: NonNullable; + + constructor( + private readonly output: PublicKnowledgeRecommendationOutput, + conversations: LettaConversation[] = [], + ) { + for (const item of conversations) this.remote.set(item.id, item); + this.conversations = { + list: async (options) => [...this.remote.values()].filter((item) => ( + (!options?.agentId || item.agent_id === options.agentId) + && (!options?.summarySearch || item.summary?.includes(options.summarySearch)) + )), + create: async (options) => { + const item = conversation(`conv-created-${++this.sequence}`, options.summary ?? ""); + this.remote.set(item.id, item); + this.created.push(item); + return item; + }, + retrieve: async (conversationId) => { + const item = this.remote.get(conversationId); + if (!item) throw new Error("conversation missing"); + return item; + }, + }; + } + + resumeSession(id: string, options?: LettaCodeClientSessionOptions): LettaCodeSession { + this.resumedIds.push(id); + this.sessionOptions.push(options); + return new FakeSession(id, this.output) as unknown as LettaCodeSession; + } +} + +class FakeSession { + readonly agentId = "agent-co-fixture"; + readonly sessionId = "session-pk-fixture"; + readonly sent: string[] = []; + closed = false; + + constructor( + readonly conversationId: string, + private readonly output: PublicKnowledgeRecommendationOutput, + ) {} + + async send(message: SendMessage): Promise { + if (typeof message !== "string") throw new Error("Fixture accepts text only"); + this.sent.push(message); + } + + async *stream(): AsyncGenerator { + yield { + type: "result", + success: true, + result: JSON.stringify(this.output), + durationMs: 20, + conversationId: this.conversationId, + runIds: ["run-pk-fixture"], + }; + } + + async abort(): Promise {} + async listMessages(): Promise { + return { messages: [], nextBefore: null, hasMore: false }; + } + close(): void { this.closed = true; } +} + +async function fixtureWorkspace(source = "# Eligible source\n\nA durable memory architecture note.\n") { + const project = await temporaryProject("thoughtstream-pk-"); + roots.push(project); + const vault = path.join(project, "vault"); + const catalog = path.join(project, "published"); + const policyPath = path.join(project, "policy.json"); + await fs.mkdir(path.join(vault, "lessons"), { recursive: true }); + await fs.mkdir(path.join(vault, "journal"), { recursive: true }); + await fs.mkdir(catalog, { recursive: true }); + await fs.writeFile(path.join(vault, "lessons", "eligible.md"), source); + await fs.writeFile(path.join(vault, "journal", "private.md"), "PRIVATE JOURNAL SENTINEL\n"); + await fs.writeFile(path.join(catalog, "agent-memory.md"), [ + "---", + "slug: agent-memory", + "title: Agent memory", + "summary: Public metadata only", + "---", + "PUBLIC BODY MUST NOT ENTER CATALOG CONTEXT", + "", + ].join("\n")); + await fs.writeFile(policyPath, JSON.stringify({ + version: 1, + defaultDecision: "deny", + sourceMappings: [{ pattern: "lessons/*.md", decision: "stage" }], + blockedPrefixes: ["journal/"], + blockedWikilinkPrefixes: ["journal/"], + })); + const store = testStore(project); + stores.push(store); + return { + project, + store, + connector: new FilesystemConnector({ id: "filesystem:coil", root: vault, privacy: "sensitive" }), + contextOptions: { policyPath, catalogRoot: catalog }, + }; +} + +function publicKnowledgeDeclaration(): ThoughtAgentDeclaration { + return { + id: "coil-public-knowledge", + version: 2, + name: "Coil Public Knowledge", + description: "Fixture", + mode: "letta-agent-sdk", + provider: "letta-local", + model: "chatgpt-plus-pro/gpt-5.6-luna", + outputContract: { ...PUBLIC_KNOWLEDGE_RECOMMENDATION_OUTPUT_CONTRACT.identity }, + lettaAgent: { + backend: "local", + agentIdEnv: "THOUGHTSTREAM_LETTA_CO_AGENT_ID", + agentId: "agent-co-fixture", + conversation: "per-document", + responseMode: "strict-json", + permissionMode: "strict", + skillSources: [], + memoryDirEnv: "THOUGHTSTREAM_LETTA_CO_MEMORY_DIR", + reasoningEffort: "low", + dreaming: { trigger: "off" }, + sandbox: { ttlMinutes: 5, terminateOnClose: false }, + }, + eventTypes: [ + "stream.thought.source.file.added", + "stream.thought.source.file.changed", + "stream.thought.source.file.renamed", + "stream.thought.source.file.deleted", + ], + compiledEventTypes: [ + "stream.thought.source.file.added", + "stream.thought.source.file.changed", + "stream.thought.source.file.renamed", + "stream.thought.source.file.deleted", + ], + sourcePatterns: ["filesystem:coil"], + acceptedPrivacy: ["sensitive"], + initialReplay: "beginning", + outputEventType: "stream.thought.derived.public-knowledge.recommendation", + emit: ["stream.thought.derived.public-knowledge.recommendation"], + promptRef: "prompts/coil-public-knowledge.md", + systemPrompt: "Recommend Public Knowledge content.", + enabled: true, + maxEvents: 1, + maxInputChars: 100_000, + contextStrategy: "coil-public-knowledge", + payloadFields: ["documentId", "path", "versionId", "sha256"], + maxOutputTokens: 2_000, + timeoutMs: 180_000, + accounting: { + leaseMs: 240_000, + reservation: { inputTokens: 60_000, outputTokens: 2_000 }, + limits: [{ + window: "hour", + maxCalls: 20, + maxInputTokens: 1_200_000, + maxOutputTokens: 40_000, + }], + }, + tools: [], + externalActions: false, + }; +} + +function validRecommendation(): PublicKnowledgeRecommendationOutput { + return { + decision: "revise-existing", + summary: "Revise the existing agent-memory entry.", + rationale: ["The source adds a useful architectural distinction."], + candidate: null, + targetSlugs: ["agent-memory"], + publicSourcesToVerify: ["Primary source"], + privacy: { status: "clear", findings: [] }, + confidence: "high", + }; +} + +function conversation(id: string, summary: string): LettaConversation { + return { + id, + agent_id: "agent-co-fixture", + summary, + } as LettaConversation; +} diff --git a/test/watch.test.ts b/test/watch.test.ts index 8507c98..7e7f53b 100644 --- a/test/watch.test.ts +++ b/test/watch.test.ts @@ -54,6 +54,38 @@ describe("thought stream watch", () => { expect(stderr).toContain("thought stream watcher stopping."); expect(stderr).toContain("thought stream watcher stopped."); }, 20_000); + + test("producer-only mode persists files without loading agent declarations", async () => { + const project = await temporaryProject(); + roots.push(project); + const vault = path.join(project, "vault"); + await fs.mkdir(vault); + await fs.mkdir(path.join(project, "agents")); + await fs.writeFile(path.join(vault, "coil.md"), "# Coil source\n"); + await fs.writeFile(path.join(project, "agents", "must-not-load.yaml"), "not: a valid declaration\n"); + const child = spawn(process.execPath, [ + "--import", "tsx", + "src/cli.ts", + "watch", + "--producer-only", + "--root", vault, + "--source", "filesystem:coil", + ], { + cwd: path.resolve(import.meta.dirname, ".."), + env: { ...process.env, THOUGHTSTREAM_ROOT: project }, + stdio: ["ignore", "pipe", "pipe"], + }); + children.push(child); + let stdout = ""; + let stderr = ""; + child.stdout!.on("data", (chunk) => { stdout += String(chunk); }); + child.stderr!.on("data", (chunk) => { stderr += String(chunk); }); + + await waitUntil(() => stdout.includes("producer-only mode"), () => stderr, 8_000); + expect(stdout).toContain('"added": 1'); + child.kill("SIGTERM"); + expect(await waitForExit(child, 8_000)).toEqual({ code: 0, signal: null }); + }, 20_000); }); async function waitUntil(predicate: () => boolean, diagnostics: () => string, timeoutMs: number): Promise {