Merge remote-tracking branch 'can1357/main' into feat/advisor-per-agent-toggle
This commit is contained in:
Generated
+26
-26
@@ -865,7 +865,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
"spin 0.10.0",
|
||||
"spin 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1376,7 +1376,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"nanorand",
|
||||
"spin 0.9.8",
|
||||
"spin 0.9.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2585,9 +2585,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
|
||||
checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
@@ -2616,9 +2616,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "napi"
|
||||
version = "3.10.4"
|
||||
version = "3.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59b7fbd5f12adbf51ddec954d4ef9cecb3542a9d53bce2fe0653696c4cd06d73"
|
||||
checksum = "6826e5ddc15589b2d68c8ad5321c18e85d40488e93e32962f362e572669bccf6"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"ctor",
|
||||
@@ -3264,7 +3264,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pi-ast"
|
||||
version = "16.4.8"
|
||||
version = "16.5.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ast-grep-core",
|
||||
@@ -3333,7 +3333,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pi-iso"
|
||||
version = "16.4.8"
|
||||
version = "16.5.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"libc",
|
||||
@@ -3345,7 +3345,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pi-natives"
|
||||
version = "16.4.8"
|
||||
version = "16.5.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
@@ -3398,7 +3398,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pi-shell"
|
||||
version = "16.4.8"
|
||||
version = "16.5.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"brush-builtins",
|
||||
@@ -3482,7 +3482,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pi-walker"
|
||||
version = "16.4.8"
|
||||
version = "16.5.0"
|
||||
dependencies = [
|
||||
"dashmap",
|
||||
"globset",
|
||||
@@ -4200,9 +4200,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.4"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
|
||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -4210,18 +4210,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
|
||||
checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
@@ -4535,7 +4535,7 @@ dependencies = [
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow 1.0.3",
|
||||
"winnow 1.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4556,7 +4556,7 @@ dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"winnow 1.0.3",
|
||||
"winnow 1.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4565,7 +4565,7 @@ version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||
dependencies = [
|
||||
"winnow 1.0.3",
|
||||
"winnow 1.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6007,9 +6007,9 @@ checksum = "0bb6d972f580f8223cb7052d8580aea2b7061e368cf476de32ea9457b19459ed"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.23.4"
|
||||
version = "1.23.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53"
|
||||
checksum = "ea5fab0d6c3c01ae70085a09cb03d4c7a1d6314e2b3e075392783396d724ca0a"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
@@ -6607,9 +6607,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
|
||||
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -6836,9 +6836,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ exclude = ["crates/vendor/brush-core", "crates/vendor/brush-builtins"]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
version = "16.4.8"
|
||||
version = "16.5.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
authors = ["Can Boluk"]
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"packages/agent": {
|
||||
"name": "@oh-my-pi/pi-agent-core",
|
||||
"version": "16.4.8",
|
||||
"version": "16.5.0",
|
||||
"dependencies": {
|
||||
"@oh-my-pi/pi-ai": "catalog:",
|
||||
"@oh-my-pi/pi-catalog": "catalog:",
|
||||
@@ -39,7 +39,7 @@
|
||||
},
|
||||
"packages/ai": {
|
||||
"name": "@oh-my-pi/pi-ai",
|
||||
"version": "16.4.8",
|
||||
"version": "16.5.0",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "catalog:",
|
||||
"@oh-my-pi/pi-catalog": "catalog:",
|
||||
@@ -55,7 +55,7 @@
|
||||
},
|
||||
"packages/catalog": {
|
||||
"name": "@oh-my-pi/pi-catalog",
|
||||
"version": "16.4.8",
|
||||
"version": "16.5.0",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "catalog:",
|
||||
"@oh-my-pi/pi-utils": "catalog:",
|
||||
@@ -69,7 +69,7 @@
|
||||
},
|
||||
"packages/coding-agent": {
|
||||
"name": "@oh-my-pi/pi-coding-agent",
|
||||
"version": "16.4.8",
|
||||
"version": "16.5.0",
|
||||
"bin": {
|
||||
"omp": "src/cli.ts",
|
||||
},
|
||||
@@ -136,16 +136,28 @@
|
||||
"@types/react-dom": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/harbor-manager": {
|
||||
"name": "@oh-my-pi/harbor-manager",
|
||||
"packages/hashline": {
|
||||
"name": "@oh-my-pi/hashline",
|
||||
"version": "16.5.0",
|
||||
"dependencies": {
|
||||
"diff": "catalog:",
|
||||
"lru-cache": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/metaharness": {
|
||||
"name": "@oh-my-pi/pi-metaharness",
|
||||
"version": "0.0.1",
|
||||
"bin": {
|
||||
"harbor-manager": "src/server.ts",
|
||||
"metaharness": "src/server.ts",
|
||||
},
|
||||
"dependencies": {
|
||||
"@oh-my-pi/hashline": "catalog:",
|
||||
"@oh-my-pi/pi-agent-core": "catalog:",
|
||||
"@oh-my-pi/pi-ai": "catalog:",
|
||||
"@oh-my-pi/pi-catalog": "catalog:",
|
||||
"@oh-my-pi/pi-coding-agent": "catalog:",
|
||||
"@oh-my-pi/pi-utils": "catalog:",
|
||||
"@oh-my-pi/typescript-edit-benchmark": "workspace:*",
|
||||
@@ -164,24 +176,12 @@
|
||||
"@types/d3-shape": "^3.1.7",
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.0",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"vite": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/hashline": {
|
||||
"name": "@oh-my-pi/hashline",
|
||||
"version": "16.4.8",
|
||||
"dependencies": {
|
||||
"diff": "catalog:",
|
||||
"lru-cache": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "catalog:",
|
||||
"react-refresh": "^0.18.0",
|
||||
},
|
||||
},
|
||||
"packages/mnemopi": {
|
||||
"name": "@oh-my-pi/pi-mnemopi",
|
||||
"version": "16.4.8",
|
||||
"version": "16.5.0",
|
||||
"bin": {
|
||||
"mnemopi": "src/cli.ts",
|
||||
},
|
||||
@@ -207,7 +207,7 @@
|
||||
},
|
||||
"packages/natives": {
|
||||
"name": "@oh-my-pi/pi-natives",
|
||||
"version": "16.4.8",
|
||||
"version": "16.5.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
@@ -215,7 +215,7 @@
|
||||
},
|
||||
"packages/snapcompact": {
|
||||
"name": "@oh-my-pi/snapcompact",
|
||||
"version": "16.4.8",
|
||||
"version": "16.5.0",
|
||||
"dependencies": {
|
||||
"@oh-my-pi/pi-ai": "catalog:",
|
||||
"@oh-my-pi/pi-natives": "catalog:",
|
||||
@@ -228,7 +228,7 @@
|
||||
},
|
||||
"packages/stats": {
|
||||
"name": "@oh-my-pi/omp-stats",
|
||||
"version": "16.4.8",
|
||||
"version": "16.5.0",
|
||||
"bin": {
|
||||
"omp-stats": "./src/index.ts",
|
||||
},
|
||||
@@ -254,7 +254,7 @@
|
||||
},
|
||||
"packages/swarm-extension": {
|
||||
"name": "@oh-my-pi/swarm-extension",
|
||||
"version": "16.4.8",
|
||||
"version": "16.5.0",
|
||||
"bin": {
|
||||
"omp-swarm": "src/cli.ts",
|
||||
},
|
||||
@@ -270,7 +270,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@oh-my-pi/pi-tui",
|
||||
"version": "16.4.8",
|
||||
"version": "16.5.0",
|
||||
"dependencies": {
|
||||
"@oh-my-pi/pi-natives": "catalog:",
|
||||
"@oh-my-pi/pi-utils": "catalog:",
|
||||
@@ -308,7 +308,7 @@
|
||||
},
|
||||
"packages/utils": {
|
||||
"name": "@oh-my-pi/pi-utils",
|
||||
"version": "16.4.8",
|
||||
"version": "16.5.0",
|
||||
"dependencies": {
|
||||
"@oh-my-pi/pi-natives": "catalog:",
|
||||
"handlebars": "catalog:",
|
||||
@@ -321,7 +321,7 @@
|
||||
},
|
||||
"packages/wire": {
|
||||
"name": "@oh-my-pi/pi-wire",
|
||||
"version": "16.4.8",
|
||||
"version": "16.5.0",
|
||||
"devDependencies": {
|
||||
"@types/bun": "catalog:",
|
||||
},
|
||||
@@ -343,93 +343,94 @@
|
||||
},
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@ark/schema@0.56.1": "patches/@ark%2Fschema@0.56.1.patch",
|
||||
"puppeteer-core@25.3.0": "patches/puppeteer-core@25.3.0.patch",
|
||||
"@ark/schema@0.56.2": "patches/@ark%2Fschema@0.56.2.patch",
|
||||
"@agentclientprotocol/sdk@1.2.1": "patches/@agentclientprotocol%2Fsdk@1.2.1.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@ark/schema": "0.56.1",
|
||||
"@ark/schema": "0.56.2",
|
||||
},
|
||||
"catalog": {
|
||||
"@agentclientprotocol/sdk": "0.25.0",
|
||||
"@agentclientprotocol/sdk": "1.2.1",
|
||||
"@babel/generator": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"@biomejs/biome": "^2.4.16",
|
||||
"@bufbuild/protobuf": "^2.12.0",
|
||||
"@bufbuild/protoc-gen-es": "^2.12.0",
|
||||
"@biomejs/biome": "^2.5.3",
|
||||
"@bufbuild/protobuf": "^2.12.1",
|
||||
"@bufbuild/protoc-gen-es": "^2.12.1",
|
||||
"@huggingface/transformers": "^4.2.0",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@napi-rs/cli": "3.7.0",
|
||||
"@oh-my-pi/hashline": "16.4.8",
|
||||
"@oh-my-pi/omp-stats": "16.4.8",
|
||||
"@oh-my-pi/pi-agent-core": "16.4.8",
|
||||
"@oh-my-pi/pi-ai": "16.4.8",
|
||||
"@oh-my-pi/pi-catalog": "16.4.8",
|
||||
"@oh-my-pi/pi-coding-agent": "16.4.8",
|
||||
"@oh-my-pi/pi-mnemopi": "16.4.8",
|
||||
"@oh-my-pi/pi-natives": "16.4.8",
|
||||
"@oh-my-pi/pi-tui": "16.4.8",
|
||||
"@oh-my-pi/pi-utils": "16.4.8",
|
||||
"@oh-my-pi/pi-wire": "16.4.8",
|
||||
"@oh-my-pi/snapcompact": "16.4.8",
|
||||
"@napi-rs/cli": "3.7.2",
|
||||
"@oh-my-pi/hashline": "16.5.0",
|
||||
"@oh-my-pi/omp-stats": "16.5.0",
|
||||
"@oh-my-pi/pi-agent-core": "16.5.0",
|
||||
"@oh-my-pi/pi-ai": "16.5.0",
|
||||
"@oh-my-pi/pi-catalog": "16.5.0",
|
||||
"@oh-my-pi/pi-coding-agent": "16.5.0",
|
||||
"@oh-my-pi/pi-mnemopi": "16.5.0",
|
||||
"@oh-my-pi/pi-natives": "16.5.0",
|
||||
"@oh-my-pi/pi-tui": "16.5.0",
|
||||
"@oh-my-pi/pi-utils": "16.5.0",
|
||||
"@oh-my-pi/pi-wire": "16.5.0",
|
||||
"@oh-my-pi/snapcompact": "16.5.0",
|
||||
"@opentelemetry/api": "^1.9.1",
|
||||
"@opentelemetry/context-async-hooks": "^2.7.1",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.220.0",
|
||||
"@opentelemetry/resources": "^2.7.1",
|
||||
"@opentelemetry/sdk-trace-base": "^2.7.1",
|
||||
"@opentelemetry/sdk-trace-node": "^2.7.1",
|
||||
"@puppeteer/browsers": "^3.0.4",
|
||||
"@tailwindcss/node": "^4.3.0",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"@puppeteer/browsers": "^3.0.6",
|
||||
"@tailwindcss/node": "^4.3.2",
|
||||
"@tailwindcss/vite": "^4.3.2",
|
||||
"@types/babel__generator": "^7.27.0",
|
||||
"@types/babel__traverse": "^7.28.0",
|
||||
"@types/bun": "^1.3.14",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/turndown": "5.0.6",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260609.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
||||
"@xterm/headless": "^6.0.0",
|
||||
"arktype": "2.2.2",
|
||||
"arktype": "2.2.3",
|
||||
"chalk": "^5.6.2",
|
||||
"chart.js": "^4.5.1",
|
||||
"date-fns": "^4.4.0",
|
||||
"diff": "^9.0.0",
|
||||
"fast-xml-parser": "^5.9.0",
|
||||
"fast-xml-parser": "^5.9.3",
|
||||
"fastembed": "2.1.0",
|
||||
"fflate": "0.8.3",
|
||||
"ghostty-web": "^0.4.0",
|
||||
"handlebars": "^4.7.9",
|
||||
"header-generator": "^2.1.82",
|
||||
"linkedom": "^0.18.12",
|
||||
"lint-staged": "^17.0.7",
|
||||
"lru-cache": "11.5.1",
|
||||
"lucide-react": "^1.17.0",
|
||||
"linkedom": "^0.18.13",
|
||||
"lint-staged": "^17.0.8",
|
||||
"lru-cache": "11.5.2",
|
||||
"lucide-react": "^1.24.0",
|
||||
"mammoth": "^1.12.0",
|
||||
"marked": "^18.0.5",
|
||||
"mupdf": "^1.27.0",
|
||||
"marked": "^18.0.6",
|
||||
"mupdf": "^1.28.0",
|
||||
"onnxruntime-node": "1.26.0",
|
||||
"postcss": "^8.5.15",
|
||||
"prettier": "^3.8.4",
|
||||
"postcss": "^8.5.16",
|
||||
"prettier": "^3.9.5",
|
||||
"puppeteer-core": "25.3.0",
|
||||
"react": "19.2.7",
|
||||
"react-chartjs-2": "^5.3.1",
|
||||
"react-dom": "19.2.7",
|
||||
"regexp-tree": "^0.1.27",
|
||||
"solid-js": "^1.9.13",
|
||||
"tailwindcss": "^4.3.0",
|
||||
"solid-js": "^1.9.14",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"ts-morph": "^28.0.0",
|
||||
"turndown": "7.2.4",
|
||||
"turndown-plugin-gfm": "1.0.2",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.0.16",
|
||||
"typescript": "^7.0.2",
|
||||
"vite": "^8.1.4",
|
||||
"vite-plugin-solid": "^2.11.12",
|
||||
"winston": "^3.19.0",
|
||||
"winston-daily-rotate-file": "^5.0.0",
|
||||
"zod": "^4",
|
||||
},
|
||||
"packages": {
|
||||
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.25.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-wU1VgXNtMvdVotX49txc3WJUDV+/QbLpsgjMvFhlRmp37osdLbI7L7y+iwAlQATwfjLxcv1r1p3ZxZBcXlGhcQ=="],
|
||||
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@1.2.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-jwYUdOQR7tc+Zfch53VL4JJyUNK/46q03uUTYb+PjECsmnNl94XFXOfYLJ8RBpMNidXd1rpOAVgb0vqD98xImA=="],
|
||||
|
||||
"@anush008/tokenizers": ["@anush008/tokenizers@0.0.0", "", { "optionalDependencies": { "@anush008/tokenizers-darwin-universal": "0.0.0", "@anush008/tokenizers-linux-x64-gnu": "0.0.0", "@anush008/tokenizers-win32-x64-msvc": "0.0.0" } }, "sha512-IQD9wkVReKAhsEAbDjh/0KrBGTEXelqZLpOBRDaIRvlzZ9sjmUP+gKbpvzyJnei2JHQiE8JAgj7YcNloINbGBw=="],
|
||||
|
||||
@@ -439,9 +440,9 @@
|
||||
|
||||
"@anush008/tokenizers-win32-x64-msvc": ["@anush008/tokenizers-win32-x64-msvc@0.0.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/5kP0G96+Cr6947F0ZetXnmL31YCaN15dbNbh2NHg7TXXRwfqk95+JtPP5Q7v4jbR2xxAmuseBqB4H/V7zKWuw=="],
|
||||
|
||||
"@ark/schema": ["@ark/schema@0.56.1", "", { "dependencies": { "@ark/util": "0.56.1" } }, "sha512-1Cf2g9nKD8K/3JGRu+gCCfYw5d4qR8YLLjDs5W5kpmaButCYWAPFUJqSXyBATPjglzCd4tIkp398iPYVs8MjRA=="],
|
||||
"@ark/schema": ["@ark/schema@0.56.2", "", { "dependencies": { "@ark/util": "0.56.2" } }, "sha512-Qx4D2JFbBWpntiHZaTv7bGG4H/M2rigiknezKg/WVyDSaLdE4YCcWAOoFB7pjjDqHbbV2OqRfntm1nnXvwMexg=="],
|
||||
|
||||
"@ark/util": ["@ark/util@0.56.1", "", {}, "sha512-Tp1rTik3q5Z+jAeeDxr5JZpmVIw0miti1ykSEHyZv5Pw3TIJl2xbN6KTacOxITp0l3s9ytlrWd30Zvqcy5vzoQ=="],
|
||||
"@ark/util": ["@ark/util@0.56.2", "", {}, "sha512-9kU2sUE38FZEGG7l3hamYMBieLYEJh2L1mrYD2eXpT+78EnQSV1bhjxJhnxGBMSTbtwpBSDNSK+K60WvaI/DTQ=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
@@ -473,10 +474,6 @@
|
||||
|
||||
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="],
|
||||
@@ -631,7 +628,7 @@
|
||||
|
||||
"@mozilla/readability": ["@mozilla/readability@0.6.0", "", {}, "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ=="],
|
||||
|
||||
"@napi-rs/cli": ["@napi-rs/cli@3.7.0", "", { "dependencies": { "@inquirer/prompts": "^8.0.0", "@napi-rs/cross-toolchain": "^1.0.3", "@napi-rs/wasm-tools": "^1.0.1", "@octokit/rest": "^22.0.1", "clipanion": "^4.0.0-rc.4", "colorette": "^2.0.20", "emnapi": "^1.10.0", "es-toolkit": "^1.41.0", "js-yaml": "^4.1.0", "obug": "^2.0.0", "semver": "^7.7.3", "typanion": "^3.14.0" }, "peerDependencies": { "@emnapi/runtime": "^1.7.1" }, "optionalPeers": ["@emnapi/runtime"], "bin": { "napi": "dist/cli.js", "napi-raw": "cli.mjs" } }, "sha512-3d3+rmxlOIV/G1zPWeX4PCxuYnhcCQM2BvY9rtimC8RO0dFR9gtYP+Grov+WoduZtfWRj5N1XvytWeRxxCk5zw=="],
|
||||
"@napi-rs/cli": ["@napi-rs/cli@3.7.2", "", { "dependencies": { "@inquirer/prompts": "^8.5.2", "@napi-rs/cross-toolchain": "^1.0.3", "@napi-rs/wasm-tools": "^1.0.1", "@octokit/rest": "^22.0.1", "clipanion": "^4.0.0-rc.4", "colorette": "^2.0.20", "emnapi": "^1.11.1", "es-toolkit": "^1.47.0", "js-yaml": "^4.2.0", "obug": "^2.1.2", "semver": "^7.8.2", "typanion": "^3.14.0" }, "peerDependencies": { "@emnapi/runtime": "^1.7.1" }, "optionalPeers": ["@emnapi/runtime"], "bin": { "napi": "dist/cli.js", "napi-raw": "cli.mjs" } }, "sha512-shDW0Td/XZQpP04Yy+OsMt1ILMKGGkoLcy1zVAsSAK0fLfWm0Upgkmfs/NOV2ZhMQwkgpR3ZEdyHmTwgrUDQuA=="],
|
||||
|
||||
"@napi-rs/cross-toolchain": ["@napi-rs/cross-toolchain@1.0.3", "", { "dependencies": { "@napi-rs/lzma": "^1.4.5", "@napi-rs/tar": "^1.1.0", "debug": "^4.4.1" }, "peerDependencies": { "@napi-rs/cross-toolchain-arm64-target-aarch64": "^1.0.3", "@napi-rs/cross-toolchain-arm64-target-armv7": "^1.0.3", "@napi-rs/cross-toolchain-arm64-target-ppc64le": "^1.0.3", "@napi-rs/cross-toolchain-arm64-target-s390x": "^1.0.3", "@napi-rs/cross-toolchain-arm64-target-x86_64": "^1.0.3", "@napi-rs/cross-toolchain-x64-target-aarch64": "^1.0.3", "@napi-rs/cross-toolchain-x64-target-armv7": "^1.0.3", "@napi-rs/cross-toolchain-x64-target-ppc64le": "^1.0.3", "@napi-rs/cross-toolchain-x64-target-s390x": "^1.0.3", "@napi-rs/cross-toolchain-x64-target-x86_64": "^1.0.3" }, "optionalPeers": ["@napi-rs/cross-toolchain-arm64-target-aarch64", "@napi-rs/cross-toolchain-arm64-target-armv7", "@napi-rs/cross-toolchain-arm64-target-ppc64le", "@napi-rs/cross-toolchain-arm64-target-s390x", "@napi-rs/cross-toolchain-arm64-target-x86_64", "@napi-rs/cross-toolchain-x64-target-aarch64", "@napi-rs/cross-toolchain-x64-target-armv7", "@napi-rs/cross-toolchain-x64-target-ppc64le", "@napi-rs/cross-toolchain-x64-target-s390x", "@napi-rs/cross-toolchain-x64-target-x86_64"] }, "sha512-ENPfLe4937bsKVTDA6zdABx4pq9w0tHqRrJHyaGxgaPq03a2Bd1unD5XSKjXJjebsABJ+MjAv1A2OvCgK9yehg=="],
|
||||
|
||||
@@ -763,8 +760,6 @@
|
||||
|
||||
"@oh-my-pi/collab-web": ["@oh-my-pi/collab-web@workspace:packages/collab-web"],
|
||||
|
||||
"@oh-my-pi/harbor-manager": ["@oh-my-pi/harbor-manager@workspace:packages/harbor-manager"],
|
||||
|
||||
"@oh-my-pi/hashline": ["@oh-my-pi/hashline@workspace:packages/hashline"],
|
||||
|
||||
"@oh-my-pi/omp-stats": ["@oh-my-pi/omp-stats@workspace:packages/stats"],
|
||||
@@ -777,6 +772,8 @@
|
||||
|
||||
"@oh-my-pi/pi-coding-agent": ["@oh-my-pi/pi-coding-agent@workspace:packages/coding-agent"],
|
||||
|
||||
"@oh-my-pi/pi-metaharness": ["@oh-my-pi/pi-metaharness@workspace:packages/metaharness"],
|
||||
|
||||
"@oh-my-pi/pi-mnemopi": ["@oh-my-pi/pi-mnemopi@workspace:packages/mnemopi"],
|
||||
|
||||
"@oh-my-pi/pi-natives": ["@oh-my-pi/pi-natives@workspace:packages/natives"],
|
||||
@@ -795,23 +792,23 @@
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
||||
|
||||
"@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.218.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw=="],
|
||||
"@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.220.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w=="],
|
||||
|
||||
"@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.9.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OQ0vzvbZBiUhjqLnUaoNfYmP8553Crr3aggB4y0ZUi815mZ7idpdJXQmoKdeBKJelYttoBlLSSHubmyw3wvX4w=="],
|
||||
|
||||
"@opentelemetry/core": ["@opentelemetry/core@2.9.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw=="],
|
||||
|
||||
"@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.218.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-exporter-base": "0.218.0", "@opentelemetry/otlp-transformer": "0.218.0", "@opentelemetry/resources": "2.7.1", "@opentelemetry/sdk-trace-base": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-r1Msf8SNLRmwh9J6XQ5uh82D7CdDWMNHnPB7LAVHjzut0TkSeKc5KcIvr4SvHvfk/xwN5gxC+VLKQ1k0o8PSPw=="],
|
||||
"@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.220.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/otlp-exporter-base": "0.220.0", "@opentelemetry/otlp-transformer": "0.220.0", "@opentelemetry/resources": "2.9.0", "@opentelemetry/sdk-trace": "2.9.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-voTAD8XgJxlK7zLkXh8EzMB09zrQr3tyY/BsnDTlDiQU/UdK58MZ63A3mUjdEDrxMjCVmBHU3WQJhRmQe+Dvzg=="],
|
||||
|
||||
"@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.218.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-transformer": "0.218.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA=="],
|
||||
"@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.220.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/otlp-transformer": "0.220.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ=="],
|
||||
|
||||
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.218.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.218.0", "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/sdk-logs": "0.218.0", "@opentelemetry/sdk-metrics": "2.7.1", "@opentelemetry/sdk-trace-base": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ=="],
|
||||
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.220.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.220.0", "@opentelemetry/core": "2.9.0", "@opentelemetry/resources": "2.9.0", "@opentelemetry/sdk-logs": "0.220.0", "@opentelemetry/sdk-metrics": "2.9.0", "@opentelemetry/sdk-trace": "2.9.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A=="],
|
||||
|
||||
"@opentelemetry/resources": ["@opentelemetry/resources@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg=="],
|
||||
|
||||
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.218.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.218.0", "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag=="],
|
||||
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.220.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.220.0", "@opentelemetry/core": "2.9.0", "@opentelemetry/resources": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA=="],
|
||||
|
||||
"@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="],
|
||||
"@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/resources": "2.9.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q=="],
|
||||
|
||||
"@opentelemetry/sdk-trace": ["@opentelemetry/sdk-trace@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/resources": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw=="],
|
||||
|
||||
@@ -819,7 +816,7 @@
|
||||
|
||||
"@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.9.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.9.0", "@opentelemetry/core": "2.9.0", "@opentelemetry/sdk-trace-base": "2.9.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ec9a7ps37huy5itYk0MalaZdSLlM6AXWp/FhtEjgMpp5leEGojBDvAl/UWttQnkMZOvFHKzRESn8TD3yKTF5nQ=="],
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.42.0", "", {}, "sha512-icc5xCzndZfhuJMy5oqk5AvloWquR7jtae74qzpkKkhGp8BivK+oCcEXgGnjCdTfp8hA44l+w8gE8yYJbocJJw=="],
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="],
|
||||
|
||||
@@ -873,7 +870,7 @@
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||
|
||||
"@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="],
|
||||
|
||||
@@ -941,26 +938,64 @@
|
||||
|
||||
"@types/turndown": ["@types/turndown@5.0.6", "", {}, "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg=="],
|
||||
|
||||
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260609.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260609.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260609.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260609.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260609.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260609.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260609.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260609.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-1HOuH/u/451O3hx4Z9fesNqarpeit6UfkgwK96sCVWi5p69F0N3v+6bI969lLIjF7K9dbYQNiWUaZ6Wik87iKg=="],
|
||||
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260707.2", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260707.2", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260707.2", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260707.2", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260707.2", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260707.2", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260707.2", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260707.2" }, "bin": { "tsgo": "bin/tsgo" } }, "sha512-oUGp+Rep/hqMhPunyinsALUwSlzHINSxitifPiSaeqoKOKD2OlR9NE3TaPqwsl4NlGslsOSUXI1JotWQzpYCPg=="],
|
||||
|
||||
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260609.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Yf/zHEadP/yUiWUdM/mZVfEVFJuGMf6nhRSFif0vp+FwtfGU4jmlpNF7BTJJdOHrrcWkwEJKzAoMCtEtyxhuyQ=="],
|
||||
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260707.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wny2pgKjGbiZtnOIHVa3tXC1UfDqxNEFzyPGmiqybedG8hipG2Nfp0l5UxbaKCjkLacUpH/W5bP2hBOMVhCOzg=="],
|
||||
|
||||
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260609.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-z4dYWI57CPHs0wV/FWFth8fWmqYH7iOm7THOfZ5Fv0jo/SWK6kE1kEUIqIAExqo7ueRNqSrCw0I8U1J4TJszAw=="],
|
||||
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260707.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Afc7M5zOwo+GpfcYwz5Z8HMB2tPVsui7nNIqEuuFB73MPdVqNn/Wmpe4tP4MRri0AtJnJknoHBaTJ/VDAp/Jhw=="],
|
||||
|
||||
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260609.1", "", { "os": "linux", "cpu": "arm" }, "sha512-mEtN8BbAgVtBu/5MVomYquXNvgok2C0KG6V0D4SV1jfBJNtlcqbp0WuIqT0bnM9DA4TgzcHvnFMpwGSK/dqI5A=="],
|
||||
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260707.2", "", { "os": "linux", "cpu": "arm" }, "sha512-hJm/UOqZTr9FHmR7uNm8VGX4oKtfWk0Jem0zPeJFNC8ckGUfSBueyiEYMZB+XmRc1aG4x1E46y3CplP4CLHvGQ=="],
|
||||
|
||||
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260609.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-OxNVWH9IhrMAzNlDyDit1dPO64GFIDPOUKoruIkJ9A1ZEONfIHXG5f+V3si9jtuNmuomiz9FjpbzOqLsgaxt+w=="],
|
||||
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260707.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-iITBa2WjjTI5N9t5l7Z4KoOSI+2zBlhbvFzsD/f8qX8QoKjz/Y4DPyBDgezYi8nkqjjksbgSOJ3/ykzhwrB9cg=="],
|
||||
|
||||
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260609.1", "", { "os": "linux", "cpu": "x64" }, "sha512-KO8WO1gBIC09T3255RlTY42TGu8en5mEkLPQu2wkMn+dX2T8KYL64zXrCeLeUWa0NvmVdJUeyWu3pFOn3zKemw=="],
|
||||
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260707.2", "", { "os": "linux", "cpu": "x64" }, "sha512-du0dzi6y97Po5vDNdPJTyyijHCpaS22JLRnKZEJXBDaO9gCIymOv/5QQokFRuOlQm0bWl3i9PF4OVdGP6uAOQA=="],
|
||||
|
||||
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260609.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-+8q19LWjnMKK6SF3PLeMEalbfWDYWHs0AU8kSFCBCke/RLoDG4FjQzVtLgUo+KWhsmZMosiEyqEnZmSlED2tIQ=="],
|
||||
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260707.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-SsAwfhyHJ1akgBc+99z4+hwdbHsdWaKB8EwCNIMA6JfSLMeUjffrYvxu+vfMyxVtOVOz7RrRXRoiDiu4a2sCtg=="],
|
||||
|
||||
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260609.1", "", { "os": "win32", "cpu": "x64" }, "sha512-qNPcss+6yRoNFfFIKQbPwJWYxDfOZwyL8JBJh4J+yMLOad/+/AOjsO4EtZsIpv5PMCjpnD75coBoDkw+5NkItw=="],
|
||||
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260707.2", "", { "os": "win32", "cpu": "x64" }, "sha512-DL4u27stv0fo71sVhOzHSwE+YMZsbBijVI+kg5dLDLilSH79WFTJ8RSQ46vJrCMt+Gjlv/JOZP1PuLJDfioYeQ=="],
|
||||
|
||||
"@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="],
|
||||
|
||||
"@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="],
|
||||
|
||||
"@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="],
|
||||
|
||||
"@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="],
|
||||
|
||||
"@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="],
|
||||
|
||||
"@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="],
|
||||
|
||||
"@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="],
|
||||
|
||||
"@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="],
|
||||
|
||||
"@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="],
|
||||
|
||||
"@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="],
|
||||
|
||||
"@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="],
|
||||
|
||||
"@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="],
|
||||
|
||||
"@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="],
|
||||
|
||||
"@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="],
|
||||
|
||||
"@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="],
|
||||
|
||||
"@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="],
|
||||
|
||||
"@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="],
|
||||
|
||||
"@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="],
|
||||
|
||||
"@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="],
|
||||
|
||||
"@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="],
|
||||
|
||||
"@typescript/vfs": ["@typescript/vfs@1.6.4", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="],
|
||||
|
||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="],
|
||||
|
||||
"@xterm/headless": ["@xterm/headless@6.0.0", "", {}, "sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw=="],
|
||||
@@ -977,9 +1012,9 @@
|
||||
|
||||
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
|
||||
"arkregex": ["arkregex@0.0.7", "", { "dependencies": { "@ark/util": "0.56.1" } }, "sha512-O/Ltrn9EUSn3ui0KVzfyrWGDUsHlzKxDVBtpQxL/6JmLRMAZAebfSNf/A/J5Ny5S6QIwrXX+RfXsu888HMs35A=="],
|
||||
"arkregex": ["arkregex@0.0.8", "", { "dependencies": { "@ark/util": "0.56.2" } }, "sha512-PJcx6G1kQTgLKPUbeYlYecDRaKq15AMSGVajlKFYWlPeJRQL+j3dKE6tyMs40HZ99djS1l9Vhl3ezAHy9JBIqQ=="],
|
||||
|
||||
"arktype": ["arktype@2.2.2", "", { "dependencies": { "@ark/schema": "0.56.1", "@ark/util": "0.56.1", "arkregex": "0.0.7" } }, "sha512-YYf1xhL2dh5aPZFlsY0RAsxv5HZqfLGLptH2ZP3JidTmsGRW8VOymhPjjMTkerL12vR2YtX0SK4c1mATtae8SA=="],
|
||||
"arktype": ["arktype@2.2.3", "", { "dependencies": { "@ark/schema": "0.56.2", "@ark/util": "0.56.2", "arkregex": "0.0.8" } }, "sha512-7W+0RLTUNJiBFIIZXwOQxSR8Z273IAd6IvqBeG9+gHnQKFsIx2C0iOtGTmMrPnlX4qLXyc5+ll7A0BIj9WrbTg=="],
|
||||
|
||||
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||
|
||||
@@ -1279,7 +1314,7 @@
|
||||
|
||||
"lop": ["lop@0.4.2", "", { "dependencies": { "duck": "^0.1.12", "option": "~0.2.1", "underscore": "^1.13.1" } }, "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="],
|
||||
"lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="],
|
||||
|
||||
"lucide-react": ["lucide-react@1.24.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA=="],
|
||||
|
||||
@@ -1495,7 +1530,7 @@
|
||||
|
||||
"typed-query-selector": ["typed-query-selector@2.12.2", "", {}, "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ=="],
|
||||
|
||||
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||
"typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],
|
||||
|
||||
"uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="],
|
||||
|
||||
@@ -1561,28 +1596,6 @@
|
||||
|
||||
"@isaacs/fs-minipass/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
||||
|
||||
"@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="],
|
||||
|
||||
"@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="],
|
||||
|
||||
"@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="],
|
||||
|
||||
"@opentelemetry/otlp-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="],
|
||||
|
||||
"@opentelemetry/otlp-transformer/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="],
|
||||
|
||||
"@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="],
|
||||
|
||||
"@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="],
|
||||
|
||||
"@opentelemetry/sdk-logs/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="],
|
||||
|
||||
"@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="],
|
||||
|
||||
"@opentelemetry/sdk-metrics/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="],
|
||||
|
||||
"@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="],
|
||||
@@ -1639,8 +1652,6 @@
|
||||
|
||||
"roarr/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
|
||||
|
||||
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||
|
||||
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"wrap-ansi/string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="],
|
||||
|
||||
@@ -248,7 +248,7 @@ fn create_windows_napi_tokio_runtime() -> Option<tokio::runtime::Runtime> {
|
||||
/// MUST stay in sync with `VERSION_SENTINEL_EXPORT` in
|
||||
/// `packages/natives/native/index.js` (which derives the name from
|
||||
/// `package.json#version`).
|
||||
#[napi(js_name = "__piNativesV16_4_8")]
|
||||
#[napi(js_name = "__piNativesV16_5_0")]
|
||||
pub const fn pi_natives_version_sentinel() {}
|
||||
|
||||
/// Native module entry point: install crash diagnostics before any tool can
|
||||
|
||||
+1
-1
@@ -167,7 +167,7 @@ Handlers and tool `execute` receive `ctx` with:
|
||||
|
||||
- `list()` — authenticated models available this session.
|
||||
- `current()` — the live session model (read lazily, so it reflects `/model` switches).
|
||||
- `resolve(spec)` — a model string (`provider/id`, bare id) or role alias (`pi/slow`, a configured role) → `Model`, honoring the same settings-backed aliases and match preferences as `--model`. Returns `undefined` when nothing matches.
|
||||
- `resolve(spec)` — a model string (`provider/id`, bare id) or role alias (`@slow`, a configured role) → `Model`, honoring the same settings-backed aliases and match preferences as `--model`. Returns `undefined` when nothing matches.
|
||||
- `family(model)` — an opaque lineage token for "same family?" checks (Claude point releases share a token; Claude and GPT differ). Compare it; don't persist it (the vocabulary tracks new releases).
|
||||
|
||||
```ts
|
||||
|
||||
@@ -75,7 +75,7 @@ they opt in.
|
||||
| flan-t5-small | Rejected — just echoes the input |
|
||||
|
||||
**Shipped local options**: `lfm2-350m`, `qwen3-0.6b`, `gemma-270m`, `qwen2.5-0.5b`, `lfm2-700m`.
|
||||
**Default**: `online` (pi/smol).
|
||||
**Default**: `online` (@smol).
|
||||
|
||||
## Task 2: Mnemopi memory (`providers.memoryModel`)
|
||||
|
||||
|
||||
+2
-2
@@ -446,9 +446,9 @@ Supported model roles:
|
||||
|
||||
- `default`, `smol`, `slow`, `vision`, `plan`, `designer`, `commit`, `tiny`, `task`, `advisor`
|
||||
|
||||
The `tiny` role overrides the online model used for lightweight background tasks (session titles, memory, `auto`-thinking difficulty classification, unexpected-stop detection); when unset, these fall back to `pi/smol`. Pick one in `/models`.
|
||||
The `tiny` role overrides the online model used for lightweight background tasks (session titles, memory, `auto`-thinking difficulty classification, unexpected-stop detection); when unset, these fall back to `@smol`. Pick one in `/models`.
|
||||
|
||||
Role aliases like `pi/smol` expand through `settings.modelRoles`. Each role value can also append a thinking selector such as `:minimal`, `:low`, `:medium`, or `:high`.
|
||||
Role aliases like `@smol` expand through `settings.modelRoles`; `*` selects `@default`. Quote `@` aliases in YAML values (`fable: "@slow"`). Each role value can also append a thinking selector such as `:minimal`, `:low`, `:medium`, or `:high`.
|
||||
|
||||
If a role points at another role, the target model still inherits normally and any explicit suffix on the referring role wins for that role-specific use.
|
||||
|
||||
|
||||
+1
-1
@@ -308,7 +308,7 @@ enabledModels:
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|---|---|---|---|
|
||||
| `modelRoles` | record | `{}` | Map of role name -> model id. Built-in roles: `default`, `smol`, `slow`, `vision`, `plan`, `designer`, `commit`, `tiny`, `task`, `advisor`. The `tiny` role overrides the online model for lightweight background tasks (titles, memory, auto-thinking, unexpected-stop), else `pi/smol`. Per-role env/flags exist only for `--model`/`--smol`/`--slow`/`--plan`; configure the advisor with `modelRoles.advisor`. |
|
||||
| `modelRoles` | record | `{}` | Map of role name -> model id. Built-in roles: `default`, `smol`, `slow`, `vision`, `plan`, `designer`, `commit`, `tiny`, `task`, `advisor`. The `tiny` role overrides the online model for lightweight background tasks (titles, memory, auto-thinking, unexpected-stop), else `@smol`. Per-role env/flags exist only for `--model`/`--smol`/`--slow`/`--plan`; configure the advisor with `modelRoles.advisor`. |
|
||||
| `modelTags` | record | `{}` | Custom role/tag metadata; can introduce additional roles. |
|
||||
| `modelProviderOrder` | array | `[]` | Preferred provider order when a model id is ambiguous. |
|
||||
| `cycleOrder` | array | `["smol","default","slow"]` | Roles cycled by the model switcher. |
|
||||
|
||||
+3
-3
@@ -179,9 +179,9 @@ Both runtimes expose `completion()` — a single stateless completion against a
|
||||
- JS: `await completion(prompt, { model?, system?, schema? })`
|
||||
- Python: `completion(prompt, *, model="default", system=None, schema=None)`
|
||||
- `model` selects a tier (default `"default"`):
|
||||
- `"smol"` → `pi/smol` role (fast / cheap)
|
||||
- `"default"` → the session's active model, falling back to the `pi/default` role
|
||||
- `"slow"` → `pi/slow` role; requests high reasoning effort only on reasoning-capable models
|
||||
- `"smol"` → `@smol` role (fast / cheap)
|
||||
- `"default"` → the session's active model, falling back to the `@default` role
|
||||
- `"slow"` → `@slow` role; requests high reasoning effort only on reasoning-capable models
|
||||
- `system` (optional) supplies a system prompt.
|
||||
- `schema` (optional) is a plain JSON-Schema object. When present, the model is forced to call a single synthetic `respond` tool with that schema (loose, non-strict), and the helper returns the parsed object. When absent, the helper returns the completion string.
|
||||
- Errors surface as exceptions: unresolved tier, missing API key, an `error`/`aborted` stop reason, or empty output each raise.
|
||||
|
||||
@@ -40,7 +40,7 @@ TUI rendering adds presentation-only truncation from `packages/coding-agent/src/
|
||||
## Flow
|
||||
1. `InspectImageTool.execute(...)` rejects immediately if `images.blockImages` is enabled in session settings.
|
||||
2. It reads `session.modelRegistry`; missing registry, empty registry, missing API key, or unresolved model each raise `ToolError` from `packages/coding-agent/src/tools/inspect-image.ts`.
|
||||
3. Model selection tries, in order, `pi/vision`, `pi/default`, the active model string from the session, then `availableModels[0]`. `expandRoleAlias(...)` and `resolveModelFromString(...)` handle each lookup.
|
||||
3. Model selection tries, in order, `@vision`, `@default`, the active model string from the session, then `availableModels[0]`. `expandRoleAlias(...)` and `resolveModelFromString(...)` handle each lookup.
|
||||
4. The chosen model must advertise `input.includes("image")`; otherwise execution fails before reading the file.
|
||||
5. `loadImageInput(...)` in `packages/coding-agent/src/utils/image-loading.ts` resolves the path with `resolveReadPath(...)`, detects MIME type with `readImageMetadata(...)`, and rejects files larger than `MAX_IMAGE_INPUT_BYTES` (`20 * 1024 * 1024`, 20 MiB) using `ImageInputTooLargeError`.
|
||||
6. `readImageMetadata(...)` in `packages/utils/src/mime.ts` inspects file headers only. Supported detected MIME types are `image/png`, `image/jpeg`, `image/gif`, and `image/webp`.
|
||||
|
||||
+41
-40
@@ -5,8 +5,9 @@
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.14",
|
||||
"patchedDependencies": {
|
||||
"@ark/schema@0.56.1": "patches/@ark%2Fschema@0.56.1.patch",
|
||||
"puppeteer-core@25.3.0": "patches/puppeteer-core@25.3.0.patch"
|
||||
"@ark/schema@0.56.2": "patches/@ark%2Fschema@0.56.2.patch",
|
||||
"puppeteer-core@25.3.0": "patches/puppeteer-core@25.3.0.patch",
|
||||
"@agentclientprotocol/sdk@1.2.1": "patches/@agentclientprotocol%2Fsdk@1.2.1.patch"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
@@ -14,79 +15,79 @@
|
||||
"python/robomp/web"
|
||||
],
|
||||
"catalog": {
|
||||
"@agentclientprotocol/sdk": "0.25.0",
|
||||
"@agentclientprotocol/sdk": "1.2.1",
|
||||
"@babel/generator": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"@biomejs/biome": "^2.4.16",
|
||||
"@bufbuild/protobuf": "^2.12.0",
|
||||
"@bufbuild/protoc-gen-es": "^2.12.0",
|
||||
"@biomejs/biome": "^2.5.3",
|
||||
"@bufbuild/protobuf": "^2.12.1",
|
||||
"@bufbuild/protoc-gen-es": "^2.12.1",
|
||||
"@huggingface/transformers": "^4.2.0",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@napi-rs/cli": "3.7.0",
|
||||
"@oh-my-pi/hashline": "16.4.8",
|
||||
"@oh-my-pi/omp-stats": "16.4.8",
|
||||
"@oh-my-pi/pi-agent-core": "16.4.8",
|
||||
"@oh-my-pi/pi-ai": "16.4.8",
|
||||
"@oh-my-pi/pi-catalog": "16.4.8",
|
||||
"@oh-my-pi/pi-coding-agent": "16.4.8",
|
||||
"@oh-my-pi/pi-mnemopi": "16.4.8",
|
||||
"@oh-my-pi/pi-natives": "16.4.8",
|
||||
"@oh-my-pi/pi-tui": "16.4.8",
|
||||
"@oh-my-pi/pi-utils": "16.4.8",
|
||||
"@oh-my-pi/pi-wire": "16.4.8",
|
||||
"@oh-my-pi/snapcompact": "16.4.8",
|
||||
"@napi-rs/cli": "3.7.2",
|
||||
"@oh-my-pi/hashline": "16.5.0",
|
||||
"@oh-my-pi/omp-stats": "16.5.0",
|
||||
"@oh-my-pi/pi-agent-core": "16.5.0",
|
||||
"@oh-my-pi/pi-ai": "16.5.0",
|
||||
"@oh-my-pi/pi-catalog": "16.5.0",
|
||||
"@oh-my-pi/pi-coding-agent": "16.5.0",
|
||||
"@oh-my-pi/pi-mnemopi": "16.5.0",
|
||||
"@oh-my-pi/pi-natives": "16.5.0",
|
||||
"@oh-my-pi/pi-tui": "16.5.0",
|
||||
"@oh-my-pi/pi-utils": "16.5.0",
|
||||
"@oh-my-pi/pi-wire": "16.5.0",
|
||||
"@oh-my-pi/snapcompact": "16.5.0",
|
||||
"@opentelemetry/api": "^1.9.1",
|
||||
"@opentelemetry/context-async-hooks": "^2.7.1",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.220.0",
|
||||
"@opentelemetry/resources": "^2.7.1",
|
||||
"@opentelemetry/sdk-trace-base": "^2.7.1",
|
||||
"@opentelemetry/sdk-trace-node": "^2.7.1",
|
||||
"@puppeteer/browsers": "^3.0.4",
|
||||
"@tailwindcss/node": "^4.3.0",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"@puppeteer/browsers": "^3.0.6",
|
||||
"@tailwindcss/node": "^4.3.2",
|
||||
"@tailwindcss/vite": "^4.3.2",
|
||||
"@types/babel__generator": "^7.27.0",
|
||||
"@types/babel__traverse": "^7.28.0",
|
||||
"@types/bun": "^1.3.14",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/turndown": "5.0.6",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260609.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
||||
"@xterm/headless": "^6.0.0",
|
||||
"arktype": "2.2.2",
|
||||
"arktype": "2.2.3",
|
||||
"chalk": "^5.6.2",
|
||||
"chart.js": "^4.5.1",
|
||||
"date-fns": "^4.4.0",
|
||||
"diff": "^9.0.0",
|
||||
"fflate": "0.8.3",
|
||||
"fastembed": "2.1.0",
|
||||
"fast-xml-parser": "^5.9.0",
|
||||
"fast-xml-parser": "^5.9.3",
|
||||
"ghostty-web": "^0.4.0",
|
||||
"handlebars": "^4.7.9",
|
||||
"header-generator": "^2.1.82",
|
||||
"linkedom": "^0.18.12",
|
||||
"lint-staged": "^17.0.7",
|
||||
"lru-cache": "11.5.1",
|
||||
"lucide-react": "^1.17.0",
|
||||
"linkedom": "^0.18.13",
|
||||
"lint-staged": "^17.0.8",
|
||||
"lru-cache": "11.5.2",
|
||||
"lucide-react": "^1.24.0",
|
||||
"mammoth": "^1.12.0",
|
||||
"marked": "^18.0.5",
|
||||
"mupdf": "^1.27.0",
|
||||
"marked": "^18.0.6",
|
||||
"mupdf": "^1.28.0",
|
||||
"onnxruntime-node": "1.26.0",
|
||||
"postcss": "^8.5.15",
|
||||
"prettier": "^3.8.4",
|
||||
"postcss": "^8.5.16",
|
||||
"prettier": "^3.9.5",
|
||||
"puppeteer-core": "25.3.0",
|
||||
"react": "19.2.7",
|
||||
"react-chartjs-2": "^5.3.1",
|
||||
"react-dom": "19.2.7",
|
||||
"regexp-tree": "^0.1.27",
|
||||
"solid-js": "^1.9.13",
|
||||
"tailwindcss": "^4.3.0",
|
||||
"solid-js": "^1.9.14",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"ts-morph": "^28.0.0",
|
||||
"turndown": "7.2.4",
|
||||
"turndown-plugin-gfm": "1.0.2",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.0.16",
|
||||
"typescript": "^7.0.2",
|
||||
"vite": "^8.1.4",
|
||||
"vite-plugin-solid": "^2.11.12",
|
||||
"winston": "^3.19.0",
|
||||
"winston-daily-rotate-file": "^5.0.0",
|
||||
@@ -94,7 +95,7 @@
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"@ark/schema": "0.56.1"
|
||||
"@ark/schema": "0.56.2"
|
||||
},
|
||||
"scripts": {
|
||||
"setup": "bun install && bun run build:native && bun --cwd=packages/coding-agent link && sh scripts/link-omp.sh",
|
||||
@@ -105,7 +106,7 @@
|
||||
"collab:relay": "bun --cwd=packages/collab-web run relay",
|
||||
"collab:mock-host": "bun --cwd=packages/collab-web run mock-host",
|
||||
"collab:web:build": "bun --cwd=packages/collab-web run build",
|
||||
"hmgr": "bun --cwd=packages/harbor-manager run dev",
|
||||
"meta": "bun --cwd=packages/metaharness run dev",
|
||||
"claude:trace": "bun scripts/claude-trace.ts",
|
||||
"build": "bun run --workspaces --if-present build",
|
||||
"build:native": "bun --cwd=packages/natives run build",
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [16.5.0] - 2026-07-13
|
||||
|
||||
### Added
|
||||
|
||||
- Added automated image-dropping rescue tier to compaction dead-end recovery
|
||||
- Added visual warnings to the session timeline when compaction fails to free sufficient space
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved compaction dead-end notifications with specific recovery instructions
|
||||
- Added an automated image-dropping rescue tier to compaction dead-end recovery.
|
||||
- Added visual warnings and detailed recovery instructions to the session timeline when compaction fails to free sufficient space.
|
||||
|
||||
## [16.4.5] - 2026-07-11
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "@oh-my-pi/pi-agent-core",
|
||||
"version": "16.4.8",
|
||||
"version": "16.5.0",
|
||||
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
||||
"homepage": "https://omp.sh",
|
||||
"author": "Can Boluk",
|
||||
|
||||
@@ -2,9 +2,23 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [16.5.0] - 2026-07-13
|
||||
|
||||
### Added
|
||||
|
||||
- Added diagnostic response headers to auth-gateway inference endpoints, including request IDs (x-request-id/request-id), LiteLLM model metadata (x-litellm-model-id/x-litellm-model-api-base), and performance/cost metrics (x-litellm-response-cost, x-litellm-response-duration-ms, openai-processing-ms) on non-streaming responses.
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated Google and Google Vertex providers to always use streamGenerateContent requests.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed empty provider responses (e.g. "Cloud Code Assist API returned an empty response") being classified as non-retryable: `ProviderResponseError` with kind `empty-body` now carries the transient flag, so session retry and configured model-fallback chains engage instead of hard-failing the turn
|
||||
- Fixed empty provider responses (such as from Cloud Code Assist API) being classified as non-retryable, allowing session retries and model-fallback chains to engage instead of failing the turn.
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed automatic /interactions chaining for follow-up turns in Google provider calls, along with the useInteractionsApi, storeInteraction, and previousInteractionId stream options.
|
||||
|
||||
## [16.4.6] - 2026-07-12
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "@oh-my-pi/pi-ai",
|
||||
"version": "16.4.8",
|
||||
"version": "16.5.0",
|
||||
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
||||
"homepage": "https://omp.sh",
|
||||
"author": "Can Boluk",
|
||||
|
||||
@@ -5,19 +5,50 @@
|
||||
* and peer-resolution logic.
|
||||
*/
|
||||
import { timingSafeEqual as nodeTimingSafeEqual } from "node:crypto";
|
||||
import type { Api, AssistantMessage, Model } from "../types";
|
||||
|
||||
const JSON_HEADERS = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
} as const;
|
||||
|
||||
export function json(status: number, body: unknown): Response {
|
||||
export function json(status: number, body: unknown, headers?: Record<string, string>): Response {
|
||||
return new Response(JSON.stringify(body) ?? "null", {
|
||||
status,
|
||||
headers: JSON_HEADERS,
|
||||
headers: headers ? { ...JSON_HEADERS, ...headers } : JSON_HEADERS,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Diagnostic response headers for translated inference requests, mirroring the
|
||||
* names existing gateway-aware clients already parse: `x-request-id` /
|
||||
* `request-id` (surfaced as `_request_id` by the OpenAI and Anthropic SDKs,
|
||||
* matches the gateway log line), LiteLLM's model-resolution and cost headers,
|
||||
* and OpenAI's `openai-processing-ms`. Model/request-id headers are always
|
||||
* present; `message` — the final assistant message, available only on
|
||||
* non-streaming responses — adds the computed cost, and `startedAt` the wall
|
||||
* time. Streaming responses send headers before usage exists, so they carry
|
||||
* only the identity headers.
|
||||
*/
|
||||
export function gatewayResponseHeaders(
|
||||
model: Model<Api>,
|
||||
info: { requestId: string; message?: AssistantMessage; startedAt?: number },
|
||||
): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
"x-request-id": info.requestId,
|
||||
"request-id": info.requestId,
|
||||
"x-litellm-model-id": model.id,
|
||||
};
|
||||
if (model.baseUrl) headers["x-litellm-model-api-base"] = model.baseUrl;
|
||||
if (info.message) headers["x-litellm-response-cost"] = info.message.usage.cost.total.toString();
|
||||
if (info.startedAt !== undefined) {
|
||||
const elapsed = (performance.now() - info.startedAt).toFixed(0);
|
||||
headers["x-litellm-response-duration-ms"] = elapsed;
|
||||
headers["openai-processing-ms"] = elapsed;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
export function resolvePeer(req: Request): string {
|
||||
const fwd = req.headers.get("x-forwarded-for");
|
||||
if (fwd) return fwd.split(",")[0].trim();
|
||||
@@ -165,6 +196,8 @@ const CORS_HEADERS: Record<string, string> = {
|
||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers":
|
||||
"authorization, content-type, anthropic-version, anthropic-beta, openai-organization, openai-project, x-stainless-*, x-api-key",
|
||||
"Access-Control-Expose-Headers":
|
||||
"x-request-id, request-id, x-litellm-model-id, x-litellm-model-api-base, x-litellm-response-cost, x-litellm-response-duration-ms, openai-processing-ms",
|
||||
"Access-Control-Max-Age": "86400",
|
||||
};
|
||||
|
||||
|
||||
@@ -32,7 +32,15 @@ import { completeSimple, streamSimple } from "../stream";
|
||||
import type { Api, AssistantMessageEventStream, Context, Model, SimpleStreamOptions } from "../types";
|
||||
import { deterministicUuid } from "../utils/deterministic-id";
|
||||
import { parseBind } from "../utils/parse-bind";
|
||||
import { captureRequestHeaders, corsHeaders, isAuthorized, json, resolvePeer, withCors } from "./http";
|
||||
import {
|
||||
captureRequestHeaders,
|
||||
corsHeaders,
|
||||
gatewayResponseHeaders,
|
||||
isAuthorized,
|
||||
json,
|
||||
resolvePeer,
|
||||
withCors,
|
||||
} from "./http";
|
||||
import type {
|
||||
AuthGatewayServerHandle,
|
||||
AuthGatewayServerOptions,
|
||||
@@ -334,6 +342,8 @@ async function handleFormatEndpoint(
|
||||
req: Request,
|
||||
peer: string,
|
||||
): Promise<Response> {
|
||||
const startedAt = performance.now();
|
||||
const requestId = crypto.randomUUID();
|
||||
const controller = mirrorRequestAbort(req);
|
||||
if (controller.signal.aborted) return clientClosedResponse(route);
|
||||
|
||||
@@ -430,6 +440,7 @@ async function handleFormatEndpoint(
|
||||
);
|
||||
|
||||
logger.info("auth-gateway request", {
|
||||
requestId,
|
||||
format: route.label,
|
||||
model: parsed.modelId,
|
||||
resolvedProvider: model.provider,
|
||||
@@ -458,7 +469,11 @@ async function handleFormatEndpoint(
|
||||
const classified = classifyGatewayError(errorMessage);
|
||||
return route.module.formatError(classified.status, classified.type, errorMessage);
|
||||
}
|
||||
return json(200, route.module.encodeResponse(message, parsed.modelId));
|
||||
return json(
|
||||
200,
|
||||
route.module.encodeResponse(message, parsed.modelId),
|
||||
gatewayResponseHeaders(model, { requestId, message, startedAt }),
|
||||
);
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) return clientClosedResponse(route);
|
||||
const classified = classifyGatewayError(error);
|
||||
@@ -493,6 +508,7 @@ async function handleFormatEndpoint(
|
||||
return new Response(sseStream, {
|
||||
status: 200,
|
||||
headers: {
|
||||
...gatewayResponseHeaders(model, { requestId }),
|
||||
"Content-Type": "text/event-stream; charset=utf-8",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
@@ -519,6 +535,8 @@ async function handleFormatEndpoint(
|
||||
* path.
|
||||
*/
|
||||
async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, peer: string): Promise<Response> {
|
||||
const startedAt = performance.now();
|
||||
const requestId = crypto.randomUUID();
|
||||
const controller = mirrorRequestAbort(req);
|
||||
const aborted = (): Response => piNative.formatError(499, "request_aborted", "client closed request");
|
||||
if (controller.signal.aborted) return aborted();
|
||||
@@ -605,6 +623,7 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
|
||||
streamOpts.sessionId ??= sessionId;
|
||||
|
||||
logger.info("auth-gateway request", {
|
||||
requestId,
|
||||
format: "pi-native",
|
||||
model: parsed.modelId,
|
||||
resolvedProvider: model.provider,
|
||||
@@ -633,7 +652,7 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
|
||||
const classified = classifyGatewayError(errorMessage);
|
||||
return piNative.formatError(classified.status, classified.type, errorMessage);
|
||||
}
|
||||
return json(200, { message });
|
||||
return json(200, { message }, gatewayResponseHeaders(model, { requestId, message, startedAt }));
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) return aborted();
|
||||
const classified = classifyGatewayError(error);
|
||||
@@ -664,6 +683,7 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
|
||||
return new Response(sseStream, {
|
||||
status: 200,
|
||||
headers: {
|
||||
...gatewayResponseHeaders(model, { requestId }),
|
||||
"Content-Type": "text/event-stream; charset=utf-8",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
*/
|
||||
|
||||
import { Buffer } from "node:buffer";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { $envpos, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
||||
@@ -329,22 +328,3 @@ export function __resetVertexTokenCache(): void {
|
||||
tokenCache.clear();
|
||||
inflight.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync best-effort probe for a usable Vertex bearer credential source — an explicit access-token
|
||||
* env var, `GOOGLE_APPLICATION_CREDENTIALS`, a user ADC file, or a GCP runtime whose metadata
|
||||
* server can mint ADC (GCE/Cloud Run/App Engine/Functions). Lets callers prefer the bearer
|
||||
* Interactions transport only when ADC is actually reachable, without paying the async
|
||||
* metadata-probe cost for API-key-only setups.
|
||||
*/
|
||||
export function hasVertexBearerCredentialsHint(): boolean {
|
||||
if (Bun.env.GOOGLE_CLOUD_ACCESS_TOKEN || Bun.env.CLOUDSDK_AUTH_ACCESS_TOKEN) return true;
|
||||
if (Bun.env.GOOGLE_APPLICATION_CREDENTIALS) return true;
|
||||
// GCP-hosted runtimes expose ADC via the metadata server; these env vars mark those runtimes.
|
||||
if (Bun.env.K_SERVICE || Bun.env.FUNCTION_TARGET || Bun.env.GAE_ENV || Bun.env.GCE_METADATA_HOST) return true;
|
||||
try {
|
||||
return fs.existsSync(userAdcPath());
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,753 +0,0 @@
|
||||
import { parseGeminiModel } from "@oh-my-pi/pi-catalog/identity";
|
||||
import { calculateCost } from "@oh-my-pi/pi-catalog/models";
|
||||
import { fetchWithRetry, readSseJson } from "@oh-my-pi/pi-utils";
|
||||
import * as AIError from "../error";
|
||||
import type {
|
||||
AssistantMessage,
|
||||
Context,
|
||||
FetchImpl,
|
||||
ImageContent,
|
||||
Message,
|
||||
Model,
|
||||
ProviderSessionState,
|
||||
TextContent,
|
||||
ToolCall,
|
||||
Usage,
|
||||
} from "../types";
|
||||
import { shouldSendServiceTier } from "../types";
|
||||
import { normalizeSystemPrompts } from "../utils";
|
||||
import { AssistantMessageEventStream } from "../utils/event-stream";
|
||||
import { convertTools, type GoogleSharedStreamOptions, type GoogleThinkingLevel } from "./google-shared";
|
||||
|
||||
type GoogleInteractionsApi = "google-generative-ai" | "google-vertex";
|
||||
type GoogleInteractionsModel = Model<GoogleInteractionsApi>;
|
||||
type GoogleOptions = GoogleSharedStreamOptions;
|
||||
|
||||
const GOOGLE_INTERACTIONS_STATE_KEY = "google-interactions-state";
|
||||
|
||||
/** Provider session state storing the last Gemini Interactions response id. */
|
||||
export interface GoogleInteractionsProviderSessionState extends ProviderSessionState {
|
||||
lastInteractionId?: string;
|
||||
}
|
||||
|
||||
/** Conversation anchor for continuing an Interactions turn from a prior assistant response. */
|
||||
export interface InteractionAnchor {
|
||||
id?: string;
|
||||
messageIndex?: number;
|
||||
}
|
||||
|
||||
type InteractionContent = { type: "text"; text: string } | { type: "image"; data: string; mime_type: string };
|
||||
|
||||
interface InteractionUserInputStep {
|
||||
type: "user_input";
|
||||
content: InteractionContent[];
|
||||
}
|
||||
|
||||
interface InteractionModelOutputStep {
|
||||
type: "model_output";
|
||||
content: InteractionContent[];
|
||||
}
|
||||
|
||||
interface InteractionFunctionCallStep {
|
||||
type: "function_call";
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface InteractionFunctionResultStep {
|
||||
type: "function_result";
|
||||
name: string;
|
||||
call_id: string;
|
||||
result: InteractionContent[];
|
||||
is_error?: boolean;
|
||||
}
|
||||
|
||||
interface InteractionThoughtStep {
|
||||
type: "thought";
|
||||
summary?: InteractionContent[];
|
||||
signature?: string;
|
||||
}
|
||||
|
||||
interface InteractionThoughtSummaryDelta {
|
||||
type: "thought_summary";
|
||||
content?: InteractionContent;
|
||||
}
|
||||
|
||||
interface InteractionThoughtSignatureDelta {
|
||||
type: "thought_signature";
|
||||
signature?: string;
|
||||
}
|
||||
|
||||
interface InteractionArgumentsDelta {
|
||||
type: "arguments_delta";
|
||||
arguments?: string;
|
||||
}
|
||||
|
||||
interface PendingInteractionToolCall {
|
||||
id: string;
|
||||
name: string;
|
||||
argumentsText: string;
|
||||
argumentsObject: Record<string, unknown>;
|
||||
}
|
||||
|
||||
type InteractionInputStep =
|
||||
| InteractionUserInputStep
|
||||
| InteractionModelOutputStep
|
||||
| InteractionFunctionCallStep
|
||||
| InteractionFunctionResultStep;
|
||||
|
||||
type InteractionStep =
|
||||
| InteractionModelOutputStep
|
||||
| InteractionFunctionCallStep
|
||||
| InteractionFunctionResultStep
|
||||
| InteractionThoughtStep
|
||||
| InteractionUserInputStep;
|
||||
|
||||
type InteractionThinkingLevel = "minimal" | "low" | "medium" | "high";
|
||||
|
||||
interface InteractionGenerationConfig {
|
||||
temperature?: number;
|
||||
top_p?: number;
|
||||
top_k?: number;
|
||||
min_p?: number;
|
||||
presence_penalty?: number;
|
||||
frequency_penalty?: number;
|
||||
repetition_penalty?: number;
|
||||
max_output_tokens?: number;
|
||||
thinking_level?: InteractionThinkingLevel;
|
||||
thinking_budget?: number;
|
||||
}
|
||||
|
||||
interface GoogleInteractionRequest {
|
||||
model: string;
|
||||
input: InteractionInputStep[];
|
||||
stream: true;
|
||||
previous_interaction_id?: string;
|
||||
system_instruction?: string;
|
||||
tools?: { functionDeclarations: Record<string, unknown>[] }[];
|
||||
store?: boolean;
|
||||
generation_config?: InteractionGenerationConfig;
|
||||
service_tier?: string;
|
||||
}
|
||||
|
||||
interface InteractionUsage {
|
||||
total_input_tokens?: number;
|
||||
total_cached_tokens?: number;
|
||||
total_output_tokens?: number;
|
||||
total_thought_tokens?: number;
|
||||
total_tokens?: number;
|
||||
}
|
||||
|
||||
interface InteractionResource {
|
||||
id?: string;
|
||||
status?: string;
|
||||
usage?: InteractionUsage;
|
||||
}
|
||||
|
||||
interface InteractionStreamMetadata {
|
||||
total_usage?: InteractionUsage;
|
||||
}
|
||||
|
||||
interface InteractionSseEvent {
|
||||
event_type?: string;
|
||||
index?: number;
|
||||
step?: InteractionStep;
|
||||
delta?:
|
||||
| InteractionContent
|
||||
| InteractionFunctionCallStep
|
||||
| InteractionThoughtStep
|
||||
| InteractionThoughtSummaryDelta
|
||||
| InteractionThoughtSignatureDelta
|
||||
| InteractionArgumentsDelta;
|
||||
interaction?: InteractionResource;
|
||||
interaction_id?: string;
|
||||
status?: string;
|
||||
metadata?: InteractionStreamMetadata;
|
||||
error?: { message?: string; code?: string | number };
|
||||
}
|
||||
|
||||
function emptyUsage(): Usage {
|
||||
return {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
function getGoogleInteractionsState(
|
||||
providerSessionState: Map<string, ProviderSessionState> | undefined,
|
||||
create: boolean,
|
||||
): GoogleInteractionsProviderSessionState | undefined {
|
||||
if (!providerSessionState) return undefined;
|
||||
const existing = providerSessionState.get(GOOGLE_INTERACTIONS_STATE_KEY) as
|
||||
| GoogleInteractionsProviderSessionState
|
||||
| undefined;
|
||||
if (existing || !create) return existing;
|
||||
const state: GoogleInteractionsProviderSessionState = { close: () => {} };
|
||||
providerSessionState.set(GOOGLE_INTERACTIONS_STATE_KEY, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
function interactionContentFromText(text: string): InteractionContent[] {
|
||||
return text.length === 0 ? [] : [{ type: "text", text }];
|
||||
}
|
||||
|
||||
function interactionContentFromParts(parts: readonly (TextContent | ImageContent)[]): InteractionContent[] {
|
||||
const content: InteractionContent[] = [];
|
||||
for (const part of parts) {
|
||||
if (part.type === "text") {
|
||||
if (part.text.length > 0) content.push({ type: "text", text: part.text });
|
||||
} else {
|
||||
content.push({ type: "image", data: part.data, mime_type: part.mimeType });
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
function userInputStepFromMessage(message: Extract<Message, { role: "user" | "developer" }>): InteractionUserInputStep {
|
||||
const content =
|
||||
typeof message.content === "string"
|
||||
? interactionContentFromText(message.content)
|
||||
: interactionContentFromParts(message.content);
|
||||
return { type: "user_input", content };
|
||||
}
|
||||
|
||||
function functionResultStepFromMessage(
|
||||
message: Extract<Message, { role: "toolResult" }>,
|
||||
): InteractionFunctionResultStep {
|
||||
const result = interactionContentFromParts(message.content);
|
||||
return {
|
||||
type: "function_result",
|
||||
name: message.toolName,
|
||||
call_id: message.toolCallId,
|
||||
result: result.length > 0 ? result : [{ type: "text", text: "" }],
|
||||
...(message.isError ? { is_error: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function appendAssistantInteractionSteps(message: AssistantMessage, steps: InteractionInputStep[]): void {
|
||||
let modelContent: InteractionContent[] = [];
|
||||
const flushModelContent = (): void => {
|
||||
if (modelContent.length === 0) return;
|
||||
steps.push({ type: "model_output", content: modelContent });
|
||||
modelContent = [];
|
||||
};
|
||||
|
||||
for (const block of message.content) {
|
||||
if (block.type === "text") {
|
||||
if (block.text.length > 0) modelContent.push({ type: "text", text: block.text });
|
||||
} else if (block.type === "toolCall") {
|
||||
flushModelContent();
|
||||
steps.push({ type: "function_call", id: block.id, name: block.name, arguments: block.arguments });
|
||||
}
|
||||
}
|
||||
flushModelContent();
|
||||
}
|
||||
|
||||
function interactionMessagesAfterAnchor(
|
||||
messages: readonly Message[],
|
||||
anchorIndex: number | undefined,
|
||||
): readonly Message[] {
|
||||
return anchorIndex === undefined ? messages : messages.slice(anchorIndex + 1);
|
||||
}
|
||||
|
||||
function buildInteractionInput(context: Context, anchorIndex: number | undefined): InteractionInputStep[] {
|
||||
const input: InteractionInputStep[] = [];
|
||||
for (const message of interactionMessagesAfterAnchor(context.messages, anchorIndex)) {
|
||||
if (message.role === "user" || message.role === "developer") {
|
||||
const step = userInputStepFromMessage(message);
|
||||
if (step.content.length > 0) input.push(step);
|
||||
} else if (message.role === "toolResult") {
|
||||
input.push(functionResultStepFromMessage(message));
|
||||
} else if (anchorIndex === undefined) {
|
||||
appendAssistantInteractionSteps(message, input);
|
||||
}
|
||||
}
|
||||
return input.length > 0 ? input : [{ type: "user_input", content: [{ type: "text", text: "" }] }];
|
||||
}
|
||||
|
||||
function toInteractionThinkingLevel(level: GoogleThinkingLevel): InteractionThinkingLevel | undefined {
|
||||
switch (level) {
|
||||
case "MINIMAL":
|
||||
return "minimal";
|
||||
case "LOW":
|
||||
return "low";
|
||||
case "MEDIUM":
|
||||
return "medium";
|
||||
case "HIGH":
|
||||
return "high";
|
||||
case "THINKING_LEVEL_UNSPECIFIED":
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function buildInteractionGenerationConfig(options: GoogleOptions | undefined): InteractionGenerationConfig | undefined {
|
||||
const config: InteractionGenerationConfig = {};
|
||||
if (options?.temperature !== undefined) config.temperature = options.temperature;
|
||||
if (options?.topP !== undefined) config.top_p = options.topP;
|
||||
if (options?.topK !== undefined) config.top_k = options.topK;
|
||||
if (options?.minP !== undefined) config.min_p = options.minP;
|
||||
if (options?.presencePenalty !== undefined) config.presence_penalty = options.presencePenalty;
|
||||
if (options?.frequencyPenalty !== undefined) config.frequency_penalty = options.frequencyPenalty;
|
||||
if (options?.repetitionPenalty !== undefined) config.repetition_penalty = options.repetitionPenalty;
|
||||
if (options?.maxTokens !== undefined) config.max_output_tokens = options.maxTokens;
|
||||
if (options?.thinking?.level !== undefined) {
|
||||
const thinkingLevel = toInteractionThinkingLevel(options.thinking.level);
|
||||
if (thinkingLevel !== undefined) config.thinking_level = thinkingLevel;
|
||||
} else if (options?.thinking?.budgetTokens !== undefined) {
|
||||
config.thinking_budget = options.thinking.budgetTokens;
|
||||
}
|
||||
return Object.keys(config).length > 0 ? config : undefined;
|
||||
}
|
||||
|
||||
function buildInteractionRequest(
|
||||
model: GoogleInteractionsModel,
|
||||
context: Context,
|
||||
options: GoogleOptions | undefined,
|
||||
anchor: InteractionAnchor,
|
||||
): GoogleInteractionRequest {
|
||||
const systemInstruction = normalizeSystemPrompts(context.systemPrompt).join("\n\n");
|
||||
const generationConfig = buildInteractionGenerationConfig(options);
|
||||
return {
|
||||
model: model.id,
|
||||
input: buildInteractionInput(context, anchor.messageIndex),
|
||||
stream: true,
|
||||
...(anchor.id !== undefined ? { previous_interaction_id: anchor.id } : {}),
|
||||
...(systemInstruction.length > 0 ? { system_instruction: systemInstruction } : {}),
|
||||
...(context.tools && context.tools.length > 0 ? { tools: convertTools(context.tools, model) } : {}),
|
||||
...(options?.storeInteraction !== undefined ? { store: options.storeInteraction } : {}),
|
||||
...(generationConfig !== undefined ? { generation_config: generationConfig } : {}),
|
||||
...(shouldSendServiceTier(options?.serviceTier, model.provider) ? { service_tier: options?.serviceTier } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function applyInteractionUsage(
|
||||
model: GoogleInteractionsModel,
|
||||
output: AssistantMessage,
|
||||
usage: InteractionUsage,
|
||||
): void {
|
||||
const thinkingTokens = usage.total_thought_tokens ?? 0;
|
||||
output.usage = {
|
||||
input: (usage.total_input_tokens ?? 0) - (usage.total_cached_tokens ?? 0),
|
||||
output: (usage.total_output_tokens ?? 0) + thinkingTokens,
|
||||
cacheRead: usage.total_cached_tokens ?? 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: usage.total_tokens ?? 0,
|
||||
...(thinkingTokens > 0 ? { reasoningTokens: thinkingTokens } : {}),
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
};
|
||||
calculateCost(model, output.usage);
|
||||
}
|
||||
|
||||
function parseInteractionFunctionCall(value: unknown): InteractionFunctionCallStep | undefined {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
||||
const record = value as Record<string, unknown>;
|
||||
if (record.type !== "function_call") return undefined;
|
||||
if (typeof record.id !== "string" || typeof record.name !== "string") return undefined;
|
||||
const args = record.arguments;
|
||||
return {
|
||||
type: "function_call",
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
arguments: args && typeof args === "object" && !Array.isArray(args) ? { ...args } : {},
|
||||
};
|
||||
}
|
||||
|
||||
function pendingToolCallFromStep(call: InteractionFunctionCallStep): PendingInteractionToolCall {
|
||||
return {
|
||||
id: call.id,
|
||||
name: call.name,
|
||||
argumentsText: "",
|
||||
argumentsObject: call.arguments,
|
||||
};
|
||||
}
|
||||
|
||||
function parseInteractionArguments(text: string): Record<string, unknown> {
|
||||
if (text.trim().length === 0) return {};
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(text);
|
||||
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return { ...parsed };
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/** Provider-specific URL, headers, and fetch implementation for an Interactions request. */
|
||||
export interface GoogleInteractionsPlan {
|
||||
url: string;
|
||||
headers: Record<string, string>;
|
||||
fetch?: FetchImpl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Streams Gemini Interactions API model-mode responses for direct Google and Vertex providers.
|
||||
*
|
||||
* `fallback`, when supplied, is the legacy `:streamGenerateContent` stream factory. It runs
|
||||
* transparently — forwarding its events into this stream — when the Interactions attempt fails
|
||||
* before any content is emitted with a signal that the model/endpoint does not support
|
||||
* Interactions (HTTP 404/400). Provide it only for auto-selected Interactions requests so an
|
||||
* explicit `useInteractionsApi: true` still surfaces failures.
|
||||
*/
|
||||
export function streamGoogleInteractions<T extends GoogleInteractionsApi>(args: {
|
||||
model: Model<T>;
|
||||
context: Context;
|
||||
options: GoogleSharedStreamOptions | undefined;
|
||||
api: T;
|
||||
anchor: InteractionAnchor;
|
||||
state: GoogleInteractionsProviderSessionState | undefined;
|
||||
prepare: () => GoogleInteractionsPlan | Promise<GoogleInteractionsPlan>;
|
||||
fallback?: () => AssistantMessageEventStream;
|
||||
}): AssistantMessageEventStream {
|
||||
const { model, context, options, anchor, state } = args;
|
||||
const stream = new AssistantMessageEventStream();
|
||||
const output: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: [],
|
||||
api: args.api,
|
||||
provider: model.provider,
|
||||
model: model.id,
|
||||
usage: emptyUsage(),
|
||||
stopReason: "stop",
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
const storeInteraction = options?.storeInteraction !== false;
|
||||
|
||||
void (async () => {
|
||||
let started = false;
|
||||
let sawTerminal = false;
|
||||
let currentTextBlock: TextContent | undefined;
|
||||
let currentThinkingBlock: Extract<AssistantMessage["content"][number], { type: "thinking" }> | undefined;
|
||||
let pendingThinkingSignature: string | undefined;
|
||||
const stepKinds = new Map<number, string>();
|
||||
const pendingToolCalls = new Map<number, PendingInteractionToolCall>();
|
||||
const ensureStarted = (): void => {
|
||||
if (started) return;
|
||||
stream.push({ type: "start", partial: output });
|
||||
started = true;
|
||||
};
|
||||
const endOpenBlocks = (): void => {
|
||||
if (currentTextBlock) {
|
||||
stream.push({
|
||||
type: "text_end",
|
||||
contentIndex: output.content.indexOf(currentTextBlock),
|
||||
content: currentTextBlock.text,
|
||||
partial: output,
|
||||
});
|
||||
currentTextBlock = undefined;
|
||||
}
|
||||
if (currentThinkingBlock) {
|
||||
stream.push({
|
||||
type: "thinking_end",
|
||||
contentIndex: output.content.indexOf(currentThinkingBlock),
|
||||
content: currentThinkingBlock.thinking,
|
||||
partial: output,
|
||||
});
|
||||
currentThinkingBlock = undefined;
|
||||
}
|
||||
};
|
||||
const emitText = (text: string): void => {
|
||||
if (text.length === 0) return;
|
||||
ensureStarted();
|
||||
if (!currentTextBlock) {
|
||||
if (currentThinkingBlock) endOpenBlocks();
|
||||
currentTextBlock = { type: "text", text: "" };
|
||||
output.content.push(currentTextBlock);
|
||||
stream.push({ type: "text_start", contentIndex: output.content.length - 1, partial: output });
|
||||
}
|
||||
currentTextBlock.text += text;
|
||||
stream.push({
|
||||
type: "text_delta",
|
||||
contentIndex: output.content.indexOf(currentTextBlock),
|
||||
delta: text,
|
||||
partial: output,
|
||||
});
|
||||
};
|
||||
const applyThinkingSignature = (signature: string | undefined): void => {
|
||||
if (!signature) return;
|
||||
if (currentThinkingBlock) {
|
||||
currentThinkingBlock.thinkingSignature = signature;
|
||||
} else {
|
||||
pendingThinkingSignature = signature;
|
||||
}
|
||||
};
|
||||
const emitThinking = (text: string): void => {
|
||||
if (text.length === 0) return;
|
||||
ensureStarted();
|
||||
if (!currentThinkingBlock) {
|
||||
if (currentTextBlock) endOpenBlocks();
|
||||
currentThinkingBlock = { type: "thinking", thinking: "", thinkingSignature: pendingThinkingSignature };
|
||||
pendingThinkingSignature = undefined;
|
||||
output.content.push(currentThinkingBlock);
|
||||
stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output });
|
||||
}
|
||||
currentThinkingBlock.thinking += text;
|
||||
stream.push({
|
||||
type: "thinking_delta",
|
||||
contentIndex: output.content.indexOf(currentThinkingBlock),
|
||||
delta: text,
|
||||
partial: output,
|
||||
});
|
||||
};
|
||||
const emitToolCall = (call: InteractionFunctionCallStep): void => {
|
||||
ensureStarted();
|
||||
endOpenBlocks();
|
||||
const toolCall: ToolCall = {
|
||||
type: "toolCall",
|
||||
id: call.id,
|
||||
name: call.name,
|
||||
arguments: call.arguments,
|
||||
};
|
||||
output.content.push(toolCall);
|
||||
const contentIndex = output.content.length - 1;
|
||||
stream.push({ type: "toolcall_start", contentIndex, partial: output });
|
||||
stream.push({
|
||||
type: "toolcall_delta",
|
||||
contentIndex,
|
||||
delta: JSON.stringify(toolCall.arguments),
|
||||
partial: output,
|
||||
});
|
||||
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
||||
};
|
||||
const emitPendingToolCall = (pending: PendingInteractionToolCall): void => {
|
||||
emitToolCall({
|
||||
type: "function_call",
|
||||
id: pending.id,
|
||||
name: pending.name,
|
||||
arguments:
|
||||
pending.argumentsText.length > 0
|
||||
? parseInteractionArguments(pending.argumentsText)
|
||||
: pending.argumentsObject,
|
||||
});
|
||||
};
|
||||
|
||||
let prepared = false;
|
||||
try {
|
||||
const plan = await args.prepare();
|
||||
prepared = true;
|
||||
let requestBody: unknown = buildInteractionRequest(model, context, options, anchor);
|
||||
const replacement = await options?.onPayload?.(requestBody, model);
|
||||
if (replacement !== undefined) requestBody = replacement;
|
||||
const response = await fetchWithRetry(() => plan.url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...plan.headers,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: options?.signal,
|
||||
fetch: plan.fetch,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => "");
|
||||
throw new AIError.GoogleApiError(
|
||||
`Google Interactions API error (${response.status}): ${errorText}`,
|
||||
response.status,
|
||||
{
|
||||
headers: response.headers,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (!response.body) {
|
||||
throw new AIError.ProviderResponseError("Google Interactions API returned an empty response body", {
|
||||
provider: model.provider,
|
||||
kind: "empty-body",
|
||||
});
|
||||
}
|
||||
for await (const event of readSseJson<InteractionSseEvent>(response.body, options?.signal, sse =>
|
||||
options?.onSseEvent?.({ event: sse.event, data: sse.data, raw: [...sse.raw] }, model),
|
||||
)) {
|
||||
if (event.error) {
|
||||
throw new AIError.ProviderResponseError(event.error.message ?? "Google Interactions API stream error", {
|
||||
provider: model.provider,
|
||||
kind: "runtime",
|
||||
});
|
||||
}
|
||||
if (event.metadata?.total_usage) applyInteractionUsage(model, output, event.metadata.total_usage);
|
||||
if (event.event_type === "interaction.created") {
|
||||
if (storeInteraction && event.interaction?.id) output.responseId = event.interaction.id;
|
||||
} else if (event.event_type === "step.start" && event.index !== undefined && event.step) {
|
||||
stepKinds.set(event.index, event.step.type);
|
||||
const call = parseInteractionFunctionCall(event.step);
|
||||
if (call) {
|
||||
pendingToolCalls.set(event.index, pendingToolCallFromStep(call));
|
||||
} else if (event.step.type === "thought") {
|
||||
applyThinkingSignature(event.step.signature);
|
||||
for (const item of event.step.summary ?? []) {
|
||||
if (item.type === "text") emitThinking(item.text);
|
||||
}
|
||||
}
|
||||
} else if (event.event_type === "step.delta" && event.index !== undefined && event.delta) {
|
||||
const call = parseInteractionFunctionCall(event.delta);
|
||||
if (call) {
|
||||
pendingToolCalls.set(event.index, pendingToolCallFromStep(call));
|
||||
} else if (event.delta.type === "text") {
|
||||
if (stepKinds.get(event.index) === "thought") emitThinking(event.delta.text);
|
||||
else emitText(event.delta.text);
|
||||
} else if (event.delta.type === "thought_summary") {
|
||||
if (event.delta.content?.type === "text") emitThinking(event.delta.content.text);
|
||||
} else if (event.delta.type === "thought_signature") {
|
||||
applyThinkingSignature(event.delta.signature);
|
||||
} else if (event.delta.type === "arguments_delta") {
|
||||
const pending = pendingToolCalls.get(event.index);
|
||||
if (pending && event.delta.arguments) pending.argumentsText += event.delta.arguments;
|
||||
}
|
||||
} else if (event.event_type === "step.stop" && event.index !== undefined) {
|
||||
const stepKind = stepKinds.get(event.index);
|
||||
const pending = pendingToolCalls.get(event.index);
|
||||
if (pending) {
|
||||
emitPendingToolCall(pending);
|
||||
pendingToolCalls.delete(event.index);
|
||||
} else {
|
||||
endOpenBlocks();
|
||||
if (stepKind === "thought") pendingThinkingSignature = undefined;
|
||||
}
|
||||
stepKinds.delete(event.index);
|
||||
} else if (event.event_type === "interaction.completed" || event.event_type === "interaction.complete") {
|
||||
if (storeInteraction && event.interaction?.id) output.responseId = event.interaction.id;
|
||||
if (event.interaction?.usage) applyInteractionUsage(model, output, event.interaction.usage);
|
||||
for (const pending of pendingToolCalls.values()) emitPendingToolCall(pending);
|
||||
pendingToolCalls.clear();
|
||||
endOpenBlocks();
|
||||
output.stopReason =
|
||||
event.interaction?.status === "requires_action" ||
|
||||
output.content.some(block => block.type === "toolCall")
|
||||
? "toolUse"
|
||||
: "stop";
|
||||
if (storeInteraction && state) state.lastInteractionId = output.responseId;
|
||||
sawTerminal = true;
|
||||
ensureStarted();
|
||||
stream.push({ type: "done", reason: output.stopReason, message: output });
|
||||
}
|
||||
}
|
||||
if (!sawTerminal) {
|
||||
throw new AIError.ProviderResponseError("Google Interactions API stream ended without a terminal event", {
|
||||
provider: model.provider,
|
||||
kind: "incomplete-stream",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Auto-selected Interactions degrades to `:streamGenerateContent` when no content has
|
||||
// streamed yet and the failure means Interactions can't serve this request — the bearer
|
||||
// credential couldn't be resolved (prepare threw) or the model/endpoint rejected it
|
||||
// (HTTP 404/400). Provider 401/403/429/5xx still surface. Mirrors the OpenAI Responses
|
||||
// `previous_response_id` fallback.
|
||||
const unsupported = error instanceof AIError.GoogleApiError && (error.status === 404 || error.status === 400);
|
||||
if (!started && args.fallback && !options?.signal?.aborted && (!prepared || unsupported)) {
|
||||
for await (const event of args.fallback()) stream.push(event);
|
||||
return;
|
||||
}
|
||||
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
||||
output.errorMessage = error instanceof Error ? error.message : String(error);
|
||||
stream.push({ type: "error", reason: output.stopReason, error: output });
|
||||
}
|
||||
})();
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
function findAssistantInteractionAnchor(
|
||||
context: Context,
|
||||
interactionId: string,
|
||||
provider: string,
|
||||
): InteractionAnchor | undefined {
|
||||
for (let index = context.messages.length - 1; index >= 0; index -= 1) {
|
||||
const message = context.messages[index];
|
||||
if (message?.role === "assistant" && message.provider === provider && message.responseId === interactionId) {
|
||||
return { id: interactionId, messageIndex: index };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function latestAssistantInteractionAnchor(context: Context, provider: string): InteractionAnchor | undefined {
|
||||
for (let index = context.messages.length - 1; index >= 0; index -= 1) {
|
||||
const message = context.messages[index];
|
||||
if (message?.role === "assistant" && message.provider === provider && message.responseId) {
|
||||
return { id: message.responseId, messageIndex: index };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveInteractionAnchor(
|
||||
context: Context,
|
||||
explicitPreviousInteractionId: string | undefined,
|
||||
state: GoogleInteractionsProviderSessionState | undefined,
|
||||
provider: string,
|
||||
): InteractionAnchor {
|
||||
if (explicitPreviousInteractionId !== undefined) {
|
||||
return (
|
||||
findAssistantInteractionAnchor(context, explicitPreviousInteractionId, provider) ?? {
|
||||
id: explicitPreviousInteractionId,
|
||||
}
|
||||
);
|
||||
}
|
||||
const lineageAnchor = latestAssistantInteractionAnchor(context, provider);
|
||||
if (lineageAnchor) return lineageAnchor;
|
||||
if (state?.lastInteractionId)
|
||||
return findAssistantInteractionAnchor(context, state.lastInteractionId, provider) ?? {};
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a model is served by the Gemini Interactions API. Interactions is a Gemini 3-era
|
||||
* transport, so the catalog subset that supports it is Gemini 3.0+. Older Gemini and non-Gemini
|
||||
* ids keep `:streamGenerateContent`, which covers the full catalog.
|
||||
*/
|
||||
export function modelSupportsInteractions(model: Pick<Model, "id">): boolean {
|
||||
const parsed = parseGeminiModel(model.id);
|
||||
return parsed !== null && parsed.version.major >= 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves whether a Google provider call should use Interactions and which lineage anchor to send.
|
||||
*
|
||||
* Precedence: explicit `useInteractionsApi: false` always wins (force generateContent); otherwise
|
||||
* Interactions engages when explicitly requested, when continuing a stored interaction
|
||||
* (`previousInteractionId`/assistant lineage/session state), or when `autoEligible` (the
|
||||
* zero-config default for the capable model subset on the official endpoint). `auto` flags the
|
||||
* last case for the caller — it is the only mode that wires up the generateContent fallback.
|
||||
*/
|
||||
export function resolveInteractionDispatch(args: {
|
||||
context: Context;
|
||||
options: GoogleSharedStreamOptions | undefined;
|
||||
provider: string;
|
||||
autoEligible: boolean;
|
||||
}): {
|
||||
useInteractions: boolean;
|
||||
auto: boolean;
|
||||
anchor: InteractionAnchor;
|
||||
state: GoogleInteractionsProviderSessionState | undefined;
|
||||
} {
|
||||
const explicitPreviousInteractionId = args.options?.previousInteractionId;
|
||||
if (args.options?.storeInteraction === false && explicitPreviousInteractionId !== undefined) {
|
||||
throw new AIError.ConfigurationError(
|
||||
"Google Interactions API cannot combine storeInteraction:false with previousInteractionId.",
|
||||
);
|
||||
}
|
||||
const explicitOptOut = args.options?.useInteractionsApi === false;
|
||||
const explicitOptIn = args.options?.useInteractionsApi === true || explicitPreviousInteractionId !== undefined;
|
||||
const storageEnabled = args.options?.storeInteraction !== false;
|
||||
const existingState = storageEnabled
|
||||
? getGoogleInteractionsState(args.options?.providerSessionState, false)
|
||||
: undefined;
|
||||
const anchor = explicitOptOut
|
||||
? {}
|
||||
: resolveInteractionAnchor(args.context, explicitPreviousInteractionId, existingState, args.provider);
|
||||
const useInteractions = !explicitOptOut && (explicitOptIn || anchor.id !== undefined || args.autoEligible);
|
||||
const auto = useInteractions && !explicitOptIn;
|
||||
const interactionState =
|
||||
useInteractions && storageEnabled
|
||||
? getGoogleInteractionsState(
|
||||
args.options?.providerSessionState,
|
||||
args.options?.providerSessionState !== undefined,
|
||||
)
|
||||
: undefined;
|
||||
return { useInteractions, auto, anchor, state: interactionState };
|
||||
}
|
||||
@@ -79,19 +79,6 @@ export interface GoogleSharedStreamOptions extends StreamOptions {
|
||||
hideThinkingSummary?: boolean;
|
||||
/** Gemini/Vertex serving tier (`flex`/`priority`); other values are omitted. */
|
||||
serviceTier?: ServiceTier;
|
||||
/**
|
||||
* Continues a Gemini Interactions API conversation from a stored interaction.
|
||||
* When set on the direct Google provider, the request uses `/interactions`
|
||||
* with `previous_interaction_id` instead of the legacy generateContent stream.
|
||||
*/
|
||||
previousInteractionId?: string;
|
||||
/**
|
||||
* Uses the Gemini Interactions API for direct Google requests, storing the
|
||||
* returned interaction id on the assistant response for follow-up turns.
|
||||
*/
|
||||
useInteractionsApi?: boolean;
|
||||
/** Overrides Interactions API request storage; default is the API default (`true`). */
|
||||
storeInteraction?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,13 +2,7 @@ import { $env } from "@oh-my-pi/pi-utils";
|
||||
import * as AIError from "../error";
|
||||
import type { Context, Model, StreamFunction } from "../types";
|
||||
import type { AssistantMessageEventStream } from "../utils/event-stream";
|
||||
import { getVertexAccessToken, hasVertexBearerCredentialsHint } from "./google-auth";
|
||||
import {
|
||||
type GoogleInteractionsPlan,
|
||||
modelSupportsInteractions,
|
||||
resolveInteractionDispatch,
|
||||
streamGoogleInteractions,
|
||||
} from "./google-interactions";
|
||||
import { getVertexAccessToken } from "./google-auth";
|
||||
import {
|
||||
buildGoogleGenerateContentParams,
|
||||
type GoogleGenAIRequestPlan,
|
||||
@@ -22,126 +16,86 @@ export interface GoogleVertexOptions extends GoogleSharedStreamOptions {
|
||||
}
|
||||
|
||||
const API_VERSION = "v1";
|
||||
const INTERACTIONS_API_VERSION = "v1beta1";
|
||||
const INTERACTIONS_API_REVISION = "2026-05-20";
|
||||
|
||||
export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
|
||||
model: Model<"google-vertex">,
|
||||
context: Context,
|
||||
options?: GoogleVertexOptions,
|
||||
): AssistantMessageEventStream => {
|
||||
const runGenerateContent = (): AssistantMessageEventStream =>
|
||||
streamGoogleGenAI({
|
||||
model,
|
||||
options,
|
||||
api: "google-vertex",
|
||||
retainTextSignature: true,
|
||||
prepare: async (): Promise<GoogleGenAIRequestPlan> => {
|
||||
const apiKey = resolveApiKey(options);
|
||||
const params = buildGoogleGenerateContentParams(model, context, options ?? {});
|
||||
params.config ||= {};
|
||||
if (!params.config.safetySettings) {
|
||||
params.config.safetySettings = [
|
||||
{
|
||||
category: "HARM_CATEGORY_HATE_SPEECH",
|
||||
threshold: "OFF",
|
||||
},
|
||||
{
|
||||
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
threshold: "OFF",
|
||||
},
|
||||
{
|
||||
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
threshold: "OFF",
|
||||
},
|
||||
{
|
||||
category: "HARM_CATEGORY_HARASSMENT",
|
||||
threshold: "OFF",
|
||||
},
|
||||
];
|
||||
}
|
||||
const baseHeaders: Record<string, string> = {
|
||||
...(model.headers ?? {}),
|
||||
...(options?.headers ?? {}),
|
||||
};
|
||||
// Vertex AI ignores a `serviceTier` request-body field (unlike the direct
|
||||
// Gemini API); priority must travel as a request header. Only `priority`
|
||||
// has a documented Vertex request control — `flex` has none, so it's a no-op.
|
||||
if (options?.serviceTier === "priority") {
|
||||
baseHeaders["X-Vertex-AI-LLM-Shared-Request-Type"] = "priority";
|
||||
}
|
||||
|
||||
if (apiKey) {
|
||||
// Explicit `location` is a deliberate residency choice: honor it and let
|
||||
// a 404 surface. An ambient env-derived region falls back to the global
|
||||
// endpoint so a stray GOOGLE_*_LOCATION never breaks a previously-working
|
||||
// global-only request.
|
||||
const explicitLocation = options?.location;
|
||||
const location = explicitLocation ?? resolveAmbientLocation() ?? "global";
|
||||
const host = resolveEndpointHost(location);
|
||||
const path = `${API_VERSION}/publishers/google/models/${model.id}:streamGenerateContent?alt=sse`;
|
||||
const useGlobalFallback = !explicitLocation && host !== "aiplatform.googleapis.com";
|
||||
return {
|
||||
params,
|
||||
url: `https://${host}/${path}`,
|
||||
fallbackUrl: useGlobalFallback ? `https://aiplatform.googleapis.com/${path}` : undefined,
|
||||
headers: {
|
||||
...baseHeaders,
|
||||
"x-goog-api-key": apiKey,
|
||||
},
|
||||
fetch: options?.fetch,
|
||||
};
|
||||
}
|
||||
|
||||
const project = resolveProject(options);
|
||||
const location = resolveLocation(options);
|
||||
const accessToken = await getVertexAccessToken({ signal: options?.signal, fetch: options?.fetch });
|
||||
const host = resolveEndpointHost(location);
|
||||
const url = `https://${host}/${API_VERSION}/projects/${project}/locations/${location}/publishers/google/models/${model.id}:streamGenerateContent?alt=sse`;
|
||||
return {
|
||||
params,
|
||||
url,
|
||||
headers: { ...baseHeaders, Authorization: `Bearer ${accessToken}` },
|
||||
fetch: options?.fetch,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Default Gemini 3+ onto Interactions whenever a bearer credential source exists (ADC file,
|
||||
// `GOOGLE_APPLICATION_CREDENTIALS`, or an explicit access-token env). Interactions needs bearer
|
||||
// auth, so express API-key-only setups stay on generateContent — and an express key, when
|
||||
// present, still serves the generateContent fallback. Interactions always targets the official
|
||||
// global `aiplatform` host; the fallback also recovers ids the endpoint rejects.
|
||||
const { useInteractions, auto, anchor, state } = resolveInteractionDispatch({
|
||||
context,
|
||||
options,
|
||||
provider: model.provider,
|
||||
autoEligible: modelSupportsInteractions(model) && hasVertexBearerCredentialsHint(),
|
||||
});
|
||||
if (!useInteractions) return runGenerateContent();
|
||||
|
||||
return streamGoogleInteractions({
|
||||
return streamGoogleGenAI({
|
||||
model,
|
||||
context,
|
||||
options,
|
||||
api: "google-vertex",
|
||||
anchor,
|
||||
state,
|
||||
prepare: async (): Promise<GoogleInteractionsPlan> => {
|
||||
retainTextSignature: true,
|
||||
prepare: async (): Promise<GoogleGenAIRequestPlan> => {
|
||||
const apiKey = resolveApiKey(options);
|
||||
const params = buildGoogleGenerateContentParams(model, context, options ?? {});
|
||||
params.config ||= {};
|
||||
if (!params.config.safetySettings) {
|
||||
params.config.safetySettings = [
|
||||
{
|
||||
category: "HARM_CATEGORY_HATE_SPEECH",
|
||||
threshold: "OFF",
|
||||
},
|
||||
{
|
||||
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
threshold: "OFF",
|
||||
},
|
||||
{
|
||||
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
threshold: "OFF",
|
||||
},
|
||||
{
|
||||
category: "HARM_CATEGORY_HARASSMENT",
|
||||
threshold: "OFF",
|
||||
},
|
||||
];
|
||||
}
|
||||
const baseHeaders: Record<string, string> = {
|
||||
...(model.headers ?? {}),
|
||||
...(options?.headers ?? {}),
|
||||
};
|
||||
// Vertex AI ignores a `serviceTier` request-body field (unlike the direct
|
||||
// Gemini API); priority must travel as a request header. Only `priority`
|
||||
// has a documented Vertex request control — `flex` has none, so it's a no-op.
|
||||
if (options?.serviceTier === "priority") {
|
||||
baseHeaders["X-Vertex-AI-LLM-Shared-Request-Type"] = "priority";
|
||||
}
|
||||
|
||||
if (apiKey) {
|
||||
// Explicit `location` is a deliberate residency choice: honor it and let
|
||||
// a 404 surface. An ambient env-derived region falls back to the global
|
||||
// endpoint so a stray GOOGLE_*_LOCATION never breaks a previously-working
|
||||
// global-only request.
|
||||
const explicitLocation = options?.location;
|
||||
const location = explicitLocation ?? resolveAmbientLocation() ?? "global";
|
||||
const host = resolveEndpointHost(location);
|
||||
const path = `${API_VERSION}/publishers/google/models/${model.id}:streamGenerateContent?alt=sse`;
|
||||
const useGlobalFallback = !explicitLocation && host !== "aiplatform.googleapis.com";
|
||||
return {
|
||||
params,
|
||||
url: `https://${host}/${path}`,
|
||||
fallbackUrl: useGlobalFallback ? `https://aiplatform.googleapis.com/${path}` : undefined,
|
||||
headers: {
|
||||
...baseHeaders,
|
||||
"x-goog-api-key": apiKey,
|
||||
},
|
||||
fetch: options?.fetch,
|
||||
};
|
||||
}
|
||||
|
||||
const project = resolveProject(options);
|
||||
const location = resolveLocation(options);
|
||||
const accessToken = await getVertexAccessToken({ signal: options?.signal, fetch: options?.fetch });
|
||||
const host = resolveEndpointHost(location);
|
||||
const url = `https://${host}/${API_VERSION}/projects/${project}/locations/${location}/publishers/google/models/${model.id}:streamGenerateContent?alt=sse`;
|
||||
return {
|
||||
url: `https://aiplatform.googleapis.com/${INTERACTIONS_API_VERSION}/projects/${project}/locations/global/interactions`,
|
||||
headers: {
|
||||
...(model.headers ?? {}),
|
||||
...(options?.headers ?? {}),
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Api-Revision": INTERACTIONS_API_REVISION,
|
||||
},
|
||||
params,
|
||||
url,
|
||||
headers: { ...baseHeaders, Authorization: `Bearer ${accessToken}` },
|
||||
fetch: options?.fetch,
|
||||
};
|
||||
},
|
||||
fallback: auto ? runGenerateContent : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import * as AIError from "../error";
|
||||
import { getEnvApiKey } from "../stream";
|
||||
import type { Context, Model, StreamFunction } from "../types";
|
||||
import type { AssistantMessageEventStream } from "../utils/event-stream";
|
||||
import { modelSupportsInteractions, resolveInteractionDispatch, streamGoogleInteractions } from "./google-interactions";
|
||||
import {
|
||||
buildGoogleGenerateContentParams,
|
||||
type GoogleGenAIRequestPlan,
|
||||
@@ -27,61 +26,22 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = (
|
||||
);
|
||||
}
|
||||
|
||||
const runGenerateContent = (): AssistantMessageEventStream =>
|
||||
streamGoogleGenAI({
|
||||
model,
|
||||
options,
|
||||
api: "google-generative-ai",
|
||||
prepare: (): GoogleGenAIRequestPlan => {
|
||||
const params = buildGoogleGenerateContentParams(model, context, options ?? {});
|
||||
// `model.baseUrl` already includes the API version segment when set (mirrors the
|
||||
// `apiVersion: ""` reset that the SDK relied on for custom base URLs).
|
||||
const base = model.baseUrl?.trim() || DEFAULT_GENERATIVE_LANGUAGE_BASE;
|
||||
const url = `${base}/models/${model.id}:streamGenerateContent?alt=sse`;
|
||||
const headers: Record<string, string> = {
|
||||
"x-goog-api-key": apiKey,
|
||||
...(model.headers ?? {}),
|
||||
...(options?.headers ?? {}),
|
||||
};
|
||||
return { params, url, headers, fetch: options?.fetch };
|
||||
},
|
||||
});
|
||||
|
||||
// Default Gemini 3+ on the official endpoint onto Interactions (custom proxy base URLs keep
|
||||
// generateContent, which serves the full catalog). The fallback recovers ids the endpoint rejects.
|
||||
const trimmedBase = model.baseUrl?.trim();
|
||||
let officialEndpoint = !trimmedBase;
|
||||
if (trimmedBase) {
|
||||
try {
|
||||
officialEndpoint = new URL(trimmedBase).hostname === "generativelanguage.googleapis.com";
|
||||
} catch {
|
||||
officialEndpoint = false;
|
||||
}
|
||||
}
|
||||
const { useInteractions, auto, anchor, state } = resolveInteractionDispatch({
|
||||
context,
|
||||
options,
|
||||
provider: model.provider,
|
||||
autoEligible: officialEndpoint && modelSupportsInteractions(model),
|
||||
});
|
||||
if (!useInteractions) return runGenerateContent();
|
||||
|
||||
return streamGoogleInteractions({
|
||||
return streamGoogleGenAI({
|
||||
model,
|
||||
context,
|
||||
options,
|
||||
api: "google-generative-ai",
|
||||
anchor,
|
||||
state,
|
||||
prepare: () => ({
|
||||
url: `${trimmedBase || DEFAULT_GENERATIVE_LANGUAGE_BASE}/interactions`,
|
||||
headers: {
|
||||
prepare: (): GoogleGenAIRequestPlan => {
|
||||
const params = buildGoogleGenerateContentParams(model, context, options ?? {});
|
||||
// `model.baseUrl` already includes the API version segment when set (mirrors the
|
||||
// `apiVersion: ""` reset that the SDK relied on for custom base URLs).
|
||||
const base = model.baseUrl?.trim() || DEFAULT_GENERATIVE_LANGUAGE_BASE;
|
||||
const url = `${base}/models/${model.id}:streamGenerateContent?alt=sse`;
|
||||
const headers: Record<string, string> = {
|
||||
"x-goog-api-key": apiKey,
|
||||
...(model.headers ?? {}),
|
||||
...(options?.headers ?? {}),
|
||||
},
|
||||
fetch: options?.fetch,
|
||||
}),
|
||||
fallback: auto ? runGenerateContent : undefined,
|
||||
};
|
||||
return { params, url, headers, fetch: options?.fetch };
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1432,9 +1432,6 @@ function mapOptionsForApi<TApi extends Api>(
|
||||
streamFirstEventTimeoutMs: options?.streamFirstEventTimeoutMs,
|
||||
streamIdleTimeoutMs: options?.streamIdleTimeoutMs,
|
||||
providerSessionState: options?.providerSessionState,
|
||||
useInteractionsApi: options?.useInteractionsApi,
|
||||
storeInteraction: options?.storeInteraction,
|
||||
previousInteractionId: options?.previousInteractionId,
|
||||
maxInFlightRequests: options?.maxInFlightRequests,
|
||||
onPayload: options?.onPayload,
|
||||
onResponse: options?.onResponse,
|
||||
|
||||
@@ -424,22 +424,6 @@ export interface StreamOptions {
|
||||
providerSessionState?: Map<string, ProviderSessionState>;
|
||||
/** Canonical Codex compaction classification; ignored by other providers. */
|
||||
codexCompaction?: CodexCompactionRequestContext;
|
||||
/**
|
||||
* Force Gemini model-mode Interactions API transport for providers that support it.
|
||||
* When unset, those providers may still use Interactions to continue known
|
||||
* server-side conversation lineage via `previousInteractionId` or stored state.
|
||||
*/
|
||||
useInteractionsApi?: boolean;
|
||||
/**
|
||||
* Whether supported Interactions transports should store server-side conversation
|
||||
* state and return response ids for follow-up turns. Defaults to true.
|
||||
*/
|
||||
storeInteraction?: boolean;
|
||||
/**
|
||||
* Explicit Interactions response id to continue. Mutually exclusive with
|
||||
* `storeInteraction: false` because the follow-up itself must be storable.
|
||||
*/
|
||||
previousInteractionId?: string;
|
||||
/**
|
||||
* Optional per-provider concurrent request cap for LLM stream calls. Keys are
|
||||
* provider ids (`model.provider`); positive numeric values cap in-flight
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { afterEach, describe, expect, it } from "bun:test";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { clearCustomApis } from "@oh-my-pi/pi-ai/api-registry";
|
||||
import { startAuthGateway } from "@oh-my-pi/pi-ai/auth-gateway";
|
||||
import { AuthStorage } from "@oh-my-pi/pi-ai/auth-storage";
|
||||
import { createMockModel, type MockModel, registerMockApi } from "@oh-my-pi/pi-ai/providers/mock";
|
||||
|
||||
interface GatewayHarness {
|
||||
url: string;
|
||||
mock: MockModel;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
async function bootGateway(): Promise<GatewayHarness> {
|
||||
registerMockApi();
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "gw-response-headers-"));
|
||||
const storage = await AuthStorage.create(path.join(dir, "auth.db"));
|
||||
storage.setRuntimeApiKey("openrouter", "test-key");
|
||||
const mock = createMockModel({ provider: "openrouter", id: "mock/header-model" });
|
||||
const handle = startAuthGateway({
|
||||
bind: "127.0.0.1:0",
|
||||
bearerTokens: ["t"],
|
||||
storage,
|
||||
resolveModel: () => mock.model,
|
||||
version: "test",
|
||||
});
|
||||
return {
|
||||
url: handle.url,
|
||||
mock,
|
||||
close: async () => {
|
||||
await handle.close();
|
||||
storage.close();
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
clearCustomApis();
|
||||
});
|
||||
|
||||
describe("auth-gateway diagnostic response headers", () => {
|
||||
it("non-streaming responses carry cost, model id, request id, and duration", async () => {
|
||||
const gw = await bootGateway();
|
||||
try {
|
||||
gw.mock.push({
|
||||
content: ["hello"],
|
||||
usage: {
|
||||
input: 10,
|
||||
output: 5,
|
||||
totalTokens: 15,
|
||||
cost: { input: 0.001, output: 0.0002, total: 0.0012 },
|
||||
},
|
||||
});
|
||||
const res = await fetch(`${gw.url}/v1/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Authorization: "Bearer t" },
|
||||
body: JSON.stringify({
|
||||
model: "mock/header-model",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
stream: false,
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get("x-litellm-response-cost")).toBe("0.0012");
|
||||
expect(res.headers.get("x-litellm-model-id")).toBe("mock/header-model");
|
||||
const duration = res.headers.get("x-litellm-response-duration-ms");
|
||||
expect(duration).not.toBeNull();
|
||||
expect(Number(duration)).toBeGreaterThanOrEqual(0);
|
||||
expect(res.headers.get("openai-processing-ms")).toBe(duration);
|
||||
const requestId = res.headers.get("x-request-id");
|
||||
expect(requestId).toMatch(/^[0-9a-f-]{36}$/);
|
||||
expect(res.headers.get("request-id")).toBe(requestId);
|
||||
} finally {
|
||||
await gw.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("streaming responses carry the model and request ids but no cost (unknown at header time)", async () => {
|
||||
const gw = await bootGateway();
|
||||
try {
|
||||
gw.mock.push({ content: ["hello"] });
|
||||
const res = await fetch(`${gw.url}/v1/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Authorization: "Bearer t" },
|
||||
body: JSON.stringify({
|
||||
model: "mock/header-model",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
stream: true,
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get("x-litellm-model-id")).toBe("mock/header-model");
|
||||
expect(res.headers.get("x-request-id")).toMatch(/^[0-9a-f-]{36}$/);
|
||||
expect(res.headers.get("x-litellm-response-cost")).toBeNull();
|
||||
await res.text();
|
||||
} finally {
|
||||
await gw.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -89,8 +89,7 @@ describe("Google empty-response retry (public + Vertex path)", () => {
|
||||
return calls === 1 ? sse(genaiChunk("")) : sse(genaiChunk("Hello!"));
|
||||
};
|
||||
|
||||
// Pin the generateContent transport: gemini-3 ids now auto-route to Interactions by default.
|
||||
const stream = streamGoogle(genaiModel, context, { apiKey: "k", fetch: fetchMock, useInteractionsApi: false });
|
||||
const stream = streamGoogle(genaiModel, context, { apiKey: "k", fetch: fetchMock });
|
||||
const { events, starts } = await drain(stream);
|
||||
const result = await stream.result();
|
||||
|
||||
@@ -108,7 +107,7 @@ describe("Google empty-response retry (public + Vertex path)", () => {
|
||||
return sse(genaiChunk(""));
|
||||
};
|
||||
|
||||
const stream = streamGoogle(genaiModel, context, { apiKey: "k", fetch: fetchMock, useInteractionsApi: false });
|
||||
const stream = streamGoogle(genaiModel, context, { apiKey: "k", fetch: fetchMock });
|
||||
const result = await stream.result();
|
||||
|
||||
expect(calls).toBe(3); // MAX_EMPTY_STREAM_RETRIES (2) + 1 initial attempt
|
||||
@@ -138,7 +137,6 @@ describe("Google empty-response retry (public + Vertex path)", () => {
|
||||
project: "project",
|
||||
location: "location",
|
||||
fetch: fetchMock,
|
||||
useInteractionsApi: false,
|
||||
});
|
||||
const { events } = await drain(stream);
|
||||
const result = await stream.result();
|
||||
@@ -196,7 +194,6 @@ describe("Google empty-response retry (public + Vertex path)", () => {
|
||||
project: "project",
|
||||
location: "location",
|
||||
fetch: fetchMock,
|
||||
useInteractionsApi: false,
|
||||
});
|
||||
const result = await stream.result();
|
||||
|
||||
|
||||
@@ -1,511 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
||||
import { streamGoogle } from "@oh-my-pi/pi-ai/providers/google";
|
||||
import { __resetVertexTokenCache } from "@oh-my-pi/pi-ai/providers/google-auth";
|
||||
import { streamGoogleVertex } from "@oh-my-pi/pi-ai/providers/google-vertex";
|
||||
import { streamSimple } from "@oh-my-pi/pi-ai/stream";
|
||||
import type { AssistantMessage, Context, FetchImpl, Model, Tool, Usage } from "@oh-my-pi/pi-ai/types";
|
||||
import { buildModel } from "@oh-my-pi/pi-catalog/build";
|
||||
|
||||
function googleModel(baseUrl = "https://generativelanguage.googleapis.com/v1beta"): Model<"google-generative-ai"> {
|
||||
return buildModel({
|
||||
id: "gemini-3.5-flash",
|
||||
name: "Gemini 3.5 Flash",
|
||||
api: "google-generative-ai",
|
||||
provider: "google",
|
||||
baseUrl,
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1_000_000,
|
||||
maxTokens: 8_192,
|
||||
});
|
||||
}
|
||||
|
||||
function vertexModel(id = "gemini-3.5-flash"): Model<"google-vertex"> {
|
||||
return buildModel({
|
||||
id,
|
||||
name: id,
|
||||
api: "google-vertex",
|
||||
provider: "google-vertex",
|
||||
baseUrl: "",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1_000_000,
|
||||
maxTokens: 8_192,
|
||||
});
|
||||
}
|
||||
|
||||
function sseResponse(events: readonly unknown[]): Response {
|
||||
const payload = `${events.map(event => `data: ${JSON.stringify(event)}`).join("\n\n")}\n\n`;
|
||||
return new Response(payload, { status: 200, headers: { "content-type": "text/event-stream" } });
|
||||
}
|
||||
|
||||
const weatherTool: Tool = {
|
||||
name: "get_weather",
|
||||
description: "Get weather",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: { city: { type: "string" } },
|
||||
required: ["city"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
};
|
||||
|
||||
describe("Google Interactions API", () => {
|
||||
it("chains tool results with previous_interaction_id from the prior assistant response", async () => {
|
||||
const model = googleModel();
|
||||
const requestBodies: unknown[] = [];
|
||||
let calls = 0;
|
||||
const fetchMock: FetchImpl = async (_input, init) => {
|
||||
requestBodies.push(JSON.parse(String(init?.body ?? "{}")));
|
||||
calls += 1;
|
||||
if (calls === 1) {
|
||||
return sseResponse([
|
||||
{
|
||||
event_type: "interaction.created",
|
||||
interaction: { id: "int_1", status: "in_progress" },
|
||||
},
|
||||
{ event_type: "step.start", index: 0, step: { type: "thought" } },
|
||||
{
|
||||
event_type: "step.delta",
|
||||
index: 0,
|
||||
delta: { type: "thought_signature", signature: "thought_sig_1" },
|
||||
},
|
||||
{
|
||||
event_type: "step.delta",
|
||||
index: 0,
|
||||
delta: { type: "thought_summary", content: { type: "text", text: "Checking weather.\n" } },
|
||||
},
|
||||
{ event_type: "step.stop", index: 0 },
|
||||
{
|
||||
event_type: "step.start",
|
||||
index: 1,
|
||||
step: {
|
||||
type: "function_call",
|
||||
id: "call_weather",
|
||||
name: "get_weather",
|
||||
arguments: {},
|
||||
},
|
||||
},
|
||||
{ event_type: "step.delta", index: 1, delta: { type: "arguments_delta", arguments: '{"city":"Bos' } },
|
||||
{ event_type: "step.delta", index: 1, delta: { type: "arguments_delta", arguments: 'ton"}' } },
|
||||
{ event_type: "step.stop", index: 1 },
|
||||
{
|
||||
event_type: "interaction.completed",
|
||||
interaction: {
|
||||
id: "int_1",
|
||||
status: "requires_action",
|
||||
usage: { total_input_tokens: 10, total_output_tokens: 2, total_tokens: 12 },
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
return sseResponse([
|
||||
{ event_type: "interaction.created", interaction: { id: "int_2", status: "in_progress" } },
|
||||
{ event_type: "step.start", index: 0, step: { type: "model_output" } },
|
||||
{ event_type: "step.delta", index: 0, delta: { type: "text", text: "Sunny." } },
|
||||
{ event_type: "step.stop", index: 0 },
|
||||
{
|
||||
event_type: "interaction.completed",
|
||||
interaction: {
|
||||
id: "int_2",
|
||||
status: "completed",
|
||||
usage: { total_input_tokens: 3, total_output_tokens: 1, total_tokens: 4 },
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
Object.assign(fetchMock, { preconnect: fetch.preconnect });
|
||||
|
||||
const firstContext: Context = {
|
||||
systemPrompt: ["Use concise weather reports."],
|
||||
messages: [{ role: "user", content: "Need weather", timestamp: 1 }],
|
||||
tools: [weatherTool],
|
||||
};
|
||||
const first = await streamGoogle(model, firstContext, {
|
||||
apiKey: "test-key",
|
||||
fetch: fetchMock,
|
||||
useInteractionsApi: true,
|
||||
thinking: { enabled: true, level: "HIGH", budgetTokens: 123 },
|
||||
}).result();
|
||||
|
||||
expect(first.responseId).toBe("int_1");
|
||||
expect(first.stopReason).toBe("toolUse");
|
||||
expect(first.content).toEqual([
|
||||
{ type: "thinking", thinking: "Checking weather.\n", thinkingSignature: "thought_sig_1" },
|
||||
{ type: "toolCall", id: "call_weather", name: "get_weather", arguments: { city: "Boston" } },
|
||||
]);
|
||||
expect(requestBodies[0]).toMatchObject({
|
||||
model: "gemini-3.5-flash",
|
||||
stream: true,
|
||||
input: [{ type: "user_input", content: [{ type: "text", text: "Need weather" }] }],
|
||||
system_instruction: "Use concise weather reports.",
|
||||
tools: [{ functionDeclarations: [{ name: "get_weather" }] }],
|
||||
generation_config: { thinking_level: "high" },
|
||||
});
|
||||
expect(requestBodies[0]).not.toHaveProperty("previous_interaction_id");
|
||||
expect(JSON.stringify(requestBodies[0])).not.toContain("thinking_budget");
|
||||
|
||||
const secondContext: Context = {
|
||||
messages: [
|
||||
{ role: "user", content: "Need weather", timestamp: 1 },
|
||||
first,
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "call_weather",
|
||||
toolName: "get_weather",
|
||||
content: [{ type: "text", text: "72F and sunny" }],
|
||||
isError: false,
|
||||
timestamp: 2,
|
||||
},
|
||||
],
|
||||
tools: [weatherTool],
|
||||
systemPrompt: ["Use concise weather reports."],
|
||||
};
|
||||
const second = await streamGoogle(model, secondContext, {
|
||||
apiKey: "test-key",
|
||||
fetch: fetchMock,
|
||||
thinking: { enabled: true, level: "HIGH", budgetTokens: 123 },
|
||||
}).result();
|
||||
|
||||
expect(second.responseId).toBe("int_2");
|
||||
expect(second.content).toEqual([{ type: "text", text: "Sunny." }]);
|
||||
expect(requestBodies[1]).toMatchObject({
|
||||
previous_interaction_id: "int_1",
|
||||
input: [
|
||||
{
|
||||
type: "function_result",
|
||||
name: "get_weather",
|
||||
call_id: "call_weather",
|
||||
result: [{ type: "text", text: "72F and sunny" }],
|
||||
},
|
||||
],
|
||||
tools: [{ functionDeclarations: [{ name: "get_weather" }] }],
|
||||
system_instruction: "Use concise weather reports.",
|
||||
generation_config: { thinking_level: "high" },
|
||||
});
|
||||
expect(JSON.stringify(requestBodies[1])).not.toContain("Need weather");
|
||||
expect(JSON.stringify(requestBodies[1])).not.toContain("thinking_budget");
|
||||
});
|
||||
|
||||
it("does not expose or reuse interaction ids when storage is disabled", async () => {
|
||||
const model = googleModel();
|
||||
const requestBodies: unknown[] = [];
|
||||
const fetchMock: FetchImpl = async (_input, init) => {
|
||||
requestBodies.push(JSON.parse(String(init?.body ?? "{}")));
|
||||
return sseResponse([
|
||||
{ event_type: "interaction.created", interaction: { id: "unstored_int", status: "in_progress" } },
|
||||
{ event_type: "step.start", index: 0, step: { type: "model_output" } },
|
||||
{ event_type: "step.delta", index: 0, delta: { type: "text", text: "Done." } },
|
||||
{ event_type: "step.stop", index: 0 },
|
||||
{ event_type: "interaction.completed", interaction: { id: "unstored_int", status: "completed" } },
|
||||
]);
|
||||
};
|
||||
Object.assign(fetchMock, { preconnect: fetch.preconnect });
|
||||
|
||||
const result = await streamGoogle(
|
||||
model,
|
||||
{ messages: [{ role: "user", content: "Hello", timestamp: 1 }] },
|
||||
{
|
||||
apiKey: "test-key",
|
||||
fetch: fetchMock,
|
||||
useInteractionsApi: true,
|
||||
storeInteraction: false,
|
||||
},
|
||||
).result();
|
||||
|
||||
expect(result.responseId).toBeUndefined();
|
||||
expect(requestBodies[0]).toMatchObject({ store: false });
|
||||
expect(() =>
|
||||
streamGoogle(
|
||||
model,
|
||||
{ messages: [{ role: "user", content: "Hello", timestamp: 1 }] },
|
||||
{
|
||||
apiKey: "test-key",
|
||||
fetch: fetchMock,
|
||||
storeInteraction: false,
|
||||
previousInteractionId: "unstored_int",
|
||||
},
|
||||
),
|
||||
).toThrow(/storeInteraction:false/);
|
||||
});
|
||||
|
||||
it("reads thought payload from step.start without leaking a prior signature", async () => {
|
||||
const model = googleModel();
|
||||
const fetchMock: FetchImpl = async () =>
|
||||
sseResponse([
|
||||
{ event_type: "interaction.created", interaction: { id: "int_3", status: "in_progress" } },
|
||||
{ event_type: "step.start", index: 0, step: { type: "thought", signature: "stale_sig" } },
|
||||
{ event_type: "step.stop", index: 0 },
|
||||
{
|
||||
event_type: "step.start",
|
||||
index: 1,
|
||||
step: { type: "thought", summary: [{ type: "text", text: "Fresh plan.\n" }] },
|
||||
},
|
||||
{ event_type: "step.stop", index: 1 },
|
||||
{ event_type: "step.start", index: 2, step: { type: "model_output" } },
|
||||
{ event_type: "step.delta", index: 2, delta: { type: "text", text: "Answer." } },
|
||||
{ event_type: "step.stop", index: 2 },
|
||||
{ event_type: "interaction.completed", interaction: { id: "int_3", status: "completed" } },
|
||||
]);
|
||||
Object.assign(fetchMock, { preconnect: fetch.preconnect });
|
||||
|
||||
const result = await streamGoogle(
|
||||
model,
|
||||
{ messages: [{ role: "user", content: "Hello", timestamp: 1 }] },
|
||||
{ apiKey: "test-key", fetch: fetchMock, useInteractionsApi: true },
|
||||
).result();
|
||||
|
||||
expect(result.content).toEqual([
|
||||
{ type: "thinking", thinking: "Fresh plan.\n" },
|
||||
{ type: "text", text: "Answer." },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
function genaiSse(text: string): Response {
|
||||
return new Response(
|
||||
`data: ${JSON.stringify({
|
||||
candidates: [{ content: { parts: [{ text }] }, finishReason: "STOP" }],
|
||||
usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 1, totalTokenCount: 2 },
|
||||
})}\n\n`,
|
||||
{ status: 200, headers: { "content-type": "text/event-stream" } },
|
||||
);
|
||||
}
|
||||
|
||||
function interactionsTextSse(
|
||||
id: string,
|
||||
text: string,
|
||||
terminal: "interaction.completed" | "interaction.complete" = "interaction.completed",
|
||||
): Response {
|
||||
return sseResponse([
|
||||
{ event_type: "interaction.created", interaction: { id, status: "in_progress" } },
|
||||
{ event_type: "step.start", index: 0, step: { type: "model_output" } },
|
||||
{ event_type: "step.delta", index: 0, delta: { type: "text", text } },
|
||||
{ event_type: "step.stop", index: 0 },
|
||||
{
|
||||
event_type: terminal,
|
||||
interaction: {
|
||||
id,
|
||||
status: "completed",
|
||||
usage: { total_input_tokens: 10, total_output_tokens: 5, total_tokens: 15 },
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
interface CapturedCall {
|
||||
url: string;
|
||||
method: string;
|
||||
headers: Headers;
|
||||
body: unknown;
|
||||
}
|
||||
|
||||
function captureFetch(handler: (url: string) => Response): { fetch: FetchImpl; calls: CapturedCall[] } {
|
||||
const calls: CapturedCall[] = [];
|
||||
const fetchMock: FetchImpl = async (input, init) => {
|
||||
const url = input instanceof Request ? input.url : String(input);
|
||||
calls.push({
|
||||
url,
|
||||
method: String(init?.method ?? "GET"),
|
||||
headers: new Headers(init?.headers),
|
||||
body: init?.body ? JSON.parse(String(init.body)) : undefined,
|
||||
});
|
||||
return handler(url);
|
||||
};
|
||||
Object.assign(fetchMock, { preconnect: fetch.preconnect });
|
||||
return { fetch: fetchMock, calls };
|
||||
}
|
||||
|
||||
const ZERO_USAGE: Usage = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
};
|
||||
|
||||
function assistantWithResponse(
|
||||
api: "google-vertex" | "google-generative-ai",
|
||||
provider: string,
|
||||
responseId: string,
|
||||
): AssistantMessage {
|
||||
return {
|
||||
role: "assistant",
|
||||
api,
|
||||
provider,
|
||||
model: "gemini-3.5-flash",
|
||||
content: [{ type: "text", text: "prev" }],
|
||||
usage: ZERO_USAGE,
|
||||
stopReason: "stop",
|
||||
timestamp: 2,
|
||||
responseId,
|
||||
};
|
||||
}
|
||||
|
||||
const userTurn: Context = { messages: [{ role: "user", content: "Hi there", timestamp: 1 }] };
|
||||
|
||||
describe("Google Interactions API — zero-config default + fallback", () => {
|
||||
let savedToken: string | undefined;
|
||||
beforeEach(() => {
|
||||
// A bearer source makes the Vertex auto-gate fire deterministically on any machine, and
|
||||
// `getVertexAccessToken` returns it directly (no OAuth/metadata round-trip in tests).
|
||||
savedToken = Bun.env.GOOGLE_CLOUD_ACCESS_TOKEN;
|
||||
Bun.env.GOOGLE_CLOUD_ACCESS_TOKEN = "test-bearer";
|
||||
});
|
||||
afterEach(() => {
|
||||
if (savedToken === undefined) delete Bun.env.GOOGLE_CLOUD_ACCESS_TOKEN;
|
||||
else Bun.env.GOOGLE_CLOUD_ACCESS_TOKEN = savedToken;
|
||||
__resetVertexTokenCache();
|
||||
});
|
||||
|
||||
it("auto-routes a capable Vertex model to Interactions under bearer auth", async () => {
|
||||
const { fetch, calls } = captureFetch(() => interactionsTextSse("vint_1", "Hi"));
|
||||
const result = await streamGoogleVertex(vertexModel(), userTurn, {
|
||||
project: "p",
|
||||
location: "us",
|
||||
fetch,
|
||||
}).result();
|
||||
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].url).toBe("https://aiplatform.googleapis.com/v1beta1/projects/p/locations/global/interactions");
|
||||
expect(calls[0].method).toBe("POST");
|
||||
expect(calls[0].headers.get("Api-Revision")).toBe("2026-05-20");
|
||||
expect(calls[0].headers.get("Authorization")).toBe("Bearer test-bearer");
|
||||
expect(calls[0].body).toMatchObject({
|
||||
model: "gemini-3.5-flash",
|
||||
stream: true,
|
||||
input: [{ type: "user_input", content: [{ type: "text", text: "Hi there" }] }],
|
||||
});
|
||||
expect(calls[0].body).not.toHaveProperty("contents");
|
||||
expect(calls[0].body).not.toHaveProperty("agent");
|
||||
expect(calls[0].body).not.toHaveProperty("environment");
|
||||
expect(result.stopReason).toBe("stop");
|
||||
expect(result.content).toEqual([{ type: "text", text: "Hi" }]);
|
||||
expect(result.responseId).toBe("vint_1");
|
||||
expect(result.usage.totalTokens).toBe(15);
|
||||
});
|
||||
|
||||
it("keeps an older (sub-3) Vertex model on generateContent", async () => {
|
||||
const { fetch, calls } = captureFetch(() => genaiSse("ok"));
|
||||
await streamGoogleVertex(vertexModel("gemini-2.5-flash"), userTurn, {
|
||||
project: "p",
|
||||
location: "us",
|
||||
fetch,
|
||||
}).result();
|
||||
|
||||
expect(calls[0].url).toContain(":streamGenerateContent");
|
||||
expect(calls.some(c => c.url.includes("/interactions"))).toBe(false);
|
||||
});
|
||||
|
||||
it("honors useInteractionsApi:false on a capable Vertex model", async () => {
|
||||
const { fetch, calls } = captureFetch(() => genaiSse("ok"));
|
||||
await streamGoogleVertex(vertexModel(), userTurn, {
|
||||
project: "p",
|
||||
location: "us",
|
||||
useInteractionsApi: false,
|
||||
fetch,
|
||||
}).result();
|
||||
|
||||
expect(calls[0].url).toContain(":streamGenerateContent");
|
||||
expect(calls.some(c => c.url.includes("/interactions"))).toBe(false);
|
||||
});
|
||||
|
||||
it("falls back to generateContent when auto Interactions is unsupported (404)", async () => {
|
||||
const { fetch, calls } = captureFetch(url =>
|
||||
url.includes("/interactions") ? new Response("nope", { status: 404 }) : genaiSse("recovered"),
|
||||
);
|
||||
const result = await streamGoogleVertex(vertexModel(), userTurn, {
|
||||
project: "p",
|
||||
location: "us",
|
||||
fetch,
|
||||
}).result();
|
||||
|
||||
expect(calls[0].url).toContain("/interactions");
|
||||
expect(calls[1].url).toContain(":streamGenerateContent");
|
||||
expect(result.stopReason).toBe("stop");
|
||||
expect(result.content).toEqual([{ type: "text", text: "recovered" }]);
|
||||
});
|
||||
|
||||
it("surfaces the error (no fallback) when explicit Interactions is unsupported", async () => {
|
||||
const { fetch, calls } = captureFetch(url =>
|
||||
url.includes("/interactions") ? new Response("nope", { status: 404 }) : genaiSse("unexpected"),
|
||||
);
|
||||
const result = await streamGoogleVertex(vertexModel(), userTurn, {
|
||||
project: "p",
|
||||
useInteractionsApi: true,
|
||||
fetch,
|
||||
}).result();
|
||||
|
||||
expect(result.stopReason).toBe("error");
|
||||
expect(calls.some(c => c.url.includes(":streamGenerateContent"))).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts interaction.complete as a terminal-event alias", async () => {
|
||||
const { fetch } = captureFetch(() => interactionsTextSse("vint_2", "Done", "interaction.complete"));
|
||||
const result = await streamGoogleVertex(vertexModel(), userTurn, { project: "p", fetch }).result();
|
||||
|
||||
expect(result.stopReason).toBe("stop");
|
||||
expect(result.content).toEqual([{ type: "text", text: "Done" }]);
|
||||
});
|
||||
|
||||
it("sends previous_interaction_id only for same-provider assistant lineage", async () => {
|
||||
const sameProvider = captureFetch(() => interactionsTextSse("vint_3", "ok"));
|
||||
await streamGoogleVertex(
|
||||
vertexModel(),
|
||||
{
|
||||
messages: [
|
||||
{ role: "user", content: "a", timestamp: 1 },
|
||||
assistantWithResponse("google-vertex", "google-vertex", "vint_prev"),
|
||||
{ role: "user", content: "b", timestamp: 3 },
|
||||
],
|
||||
},
|
||||
{ project: "p", fetch: sameProvider.fetch },
|
||||
).result();
|
||||
expect(sameProvider.calls[0].body).toMatchObject({ previous_interaction_id: "vint_prev" });
|
||||
|
||||
const wrongProvider = captureFetch(() => interactionsTextSse("vint_4", "ok"));
|
||||
await streamGoogleVertex(
|
||||
vertexModel(),
|
||||
{
|
||||
messages: [
|
||||
{ role: "user", content: "a", timestamp: 1 },
|
||||
assistantWithResponse("google-generative-ai", "google", "gint_prev"),
|
||||
{ role: "user", content: "b", timestamp: 3 },
|
||||
],
|
||||
},
|
||||
{ project: "p", fetch: wrongProvider.fetch },
|
||||
).result();
|
||||
expect(wrongProvider.calls[0].body).not.toHaveProperty("previous_interaction_id");
|
||||
});
|
||||
|
||||
it("auto-routes a capable direct Google model on the official endpoint to Interactions", async () => {
|
||||
const { fetch, calls } = captureFetch(() => interactionsTextSse("gint_1", "Hi"));
|
||||
const result = await streamGoogle(googleModel(), userTurn, { apiKey: "k", fetch }).result();
|
||||
|
||||
expect(calls[0].url).toBe("https://generativelanguage.googleapis.com/v1beta/interactions");
|
||||
expect(calls[0].headers.get("x-goog-api-key")).toBe("k");
|
||||
expect(result.responseId).toBe("gint_1");
|
||||
});
|
||||
|
||||
it("keeps a custom-baseUrl direct Google model on generateContent", async () => {
|
||||
const { fetch, calls } = captureFetch(() => genaiSse("ok"));
|
||||
await streamGoogle(googleModel("https://proxy.example.com/v1beta"), userTurn, {
|
||||
apiKey: "k",
|
||||
fetch,
|
||||
}).result();
|
||||
|
||||
expect(calls[0].url).toContain(":streamGenerateContent");
|
||||
expect(calls[0].url.startsWith("https://proxy.example.com/")).toBe(true);
|
||||
expect(calls.some(c => c.url.includes("/interactions"))).toBe(false);
|
||||
});
|
||||
|
||||
it("threads the auto-default through streamSimple for a capable Google model", async () => {
|
||||
const { fetch, calls } = captureFetch(() => interactionsTextSse("sint_1", "Hi"));
|
||||
await streamSimple(googleModel(), userTurn, { apiKey: "k", fetch }).result();
|
||||
|
||||
expect(calls.some(c => c.url.includes("/interactions"))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -75,9 +75,7 @@ const vertexModel: Model<"google-vertex"> = buildModel({
|
||||
describe("Google service tier wire encoding", () => {
|
||||
it("Gemini API sends the tier in the request body, not a header", async () => {
|
||||
const { fetch, captured } = capturingFetch();
|
||||
await drain(
|
||||
streamGoogle(geminiModel, context, { apiKey: "k", serviceTier: "priority", fetch, useInteractionsApi: false }),
|
||||
);
|
||||
await drain(streamGoogle(geminiModel, context, { apiKey: "k", serviceTier: "priority", fetch }));
|
||||
const { headers, body } = captured();
|
||||
expect(body.serviceTier).toBe("priority");
|
||||
expect(headers.get("X-Vertex-AI-LLM-Shared-Request-Type")).toBeNull();
|
||||
@@ -89,7 +87,6 @@ describe("Google service tier wire encoding", () => {
|
||||
streamGoogle(geminiModel, context, {
|
||||
apiKey: "k",
|
||||
fetch,
|
||||
useInteractionsApi: false,
|
||||
thinking: { enabled: true, level: "HIGH" },
|
||||
hideThinkingSummary: true,
|
||||
}),
|
||||
@@ -119,7 +116,7 @@ describe("Google service tier wire encoding", () => {
|
||||
|
||||
it("omits the tier entirely when unset", async () => {
|
||||
const { fetch, captured } = capturingFetch();
|
||||
await drain(streamGoogle(geminiModel, context, { apiKey: "k", fetch, useInteractionsApi: false }));
|
||||
await drain(streamGoogle(geminiModel, context, { apiKey: "k", fetch }));
|
||||
expect(captured().body.serviceTier).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,8 +26,6 @@ async function captureGooglePayload(
|
||||
|
||||
await streamGoogle(model, context, {
|
||||
apiKey: "test-key",
|
||||
// Capture the generateContent request shape; gemini-3 ids auto-route to Interactions by default.
|
||||
useInteractionsApi: false,
|
||||
onPayload: payload => {
|
||||
captured = payload as { config: { systemInstruction?: unknown }; contents: unknown[] };
|
||||
},
|
||||
|
||||
@@ -44,8 +44,6 @@ describe("issue #1270: Vertex AI global endpoint", () => {
|
||||
const stream = streamGoogleVertex(model, context, {
|
||||
project: "vertex-project",
|
||||
location: "global",
|
||||
// This asserts the generateContent URL; gemini-3 ids auto-route to Interactions by default.
|
||||
useInteractionsApi: false,
|
||||
fetch: async input => {
|
||||
const url = input instanceof Request ? input.url : input.toString();
|
||||
urls.push(url);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "@oh-my-pi/pi-catalog",
|
||||
"version": "16.4.8",
|
||||
"version": "16.5.0",
|
||||
"description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
|
||||
"homepage": "https://omp.sh",
|
||||
"author": "Can Boluk",
|
||||
|
||||
@@ -2,50 +2,51 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [16.5.0] - 2026-07-13
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Replaced the `--reasoning-slide-*` flag family (`--reasoning-slide-model`, `--reasoning-slide-turns`, `--reasoning-slide-on-action`, `--reasoning-slide-plan`, `--reasoning-slide-plan-at`, `--reasoning-slide-checklist`) with a single downshift mechanism: `--downshift` switches from the starting model to a fast/cheap target at the first completed turn that starts execution — the todo-list init the plan nudge asks for, or any edit/write tool — always with the hidden plan nudge before the switch and the verify-before-finishing checklist after it (the configuration that won benchmark testing). `--downshift-into <model>` overrides the default "smol"-role target and implies `--downshift`; `--no-downshift` force-disables. The fixed-turn trigger and per-piece plan/checklist toggles are gone.
|
||||
- Replaced the `--reasoning-slide-*` flag family with a unified `--prewalk` mechanism (`--prewalk`, `--prewalk-into <model>`, and `--no-prewalk`) to manage model handoffs during execution.
|
||||
|
||||
### Added
|
||||
|
||||
- Added `--downshift` / `--downshift-into <model>` / `--no-downshift`: start on a strong model, then hand off to a fast/cheap one (default the `smol` role) at the first edit/write tool call *after* the todo list has been initialized. The starting model handles all planning and todo initialization, and begins implementation, before handing off; the fast model includes a verify checklist before finishing. Enable per-user with the `downshift.enabled` setting; force it mid-session with the new `/downshift` slash command, which arms the switch.
|
||||
- Added display setting to toggle between collapsing or keeping compacted history inline, now applied to live session displays
|
||||
- Added a compact session-only model picker (Alt+P) for quick model switching without changing roles
|
||||
- Added `@` search to the Alt+P / `/switch` picker: it lists configured Ctrl+P quick roles in matching segment colors and applies the selected role's model and thinking for the current session.
|
||||
- Redesigned Agent Hub entries as two-line cards: identity (status glyph, name, agent type, parent when nested) on the left, active model + reasoning level and age right-aligned, with the task description on its own line; dropped the redundant `sub · of Main` noise
|
||||
- Added a project-scoped `launch` tool for shared long-running services and debuggers, with readiness probes, bounded logs, PTY input, restart policies, and automatic teardown after the last omp instance exits. Gated behind the `launch.enabled` setting (default on); when disabled the tool is withdrawn and the bash prompt drops its "use launch" guidance.
|
||||
- Added `detached` `launch` starts for standalone services that survive every omp instance and broker shutdown, then reconnect to the next broker for logs and explicit stop.
|
||||
- Added a new `--prewalk` execution flow (with `--prewalk-into <model>` and `--no-prewalk` overrides) that starts tasks on a strong model for planning and todo initialization before handing off to a faster, cheaper model for implementation.
|
||||
- Added a status line annotation for the active prewalk phase (armed or active).
|
||||
- Added the `tui.scrollbackRebuild` setting to gate the erase-and-replay native scrollback rebuild mechanism (defaults to off).
|
||||
- Added a display setting to toggle between collapsing or keeping compacted history inline in live session displays.
|
||||
- Added a compact session-only model picker (Alt+P) for quick model switching, featuring `@` search to quickly list and apply configured quick roles.
|
||||
- Redesigned Agent Hub entries into a cleaner two-line card layout showing identity, active model, reasoning level, age, and task description.
|
||||
- Added a project-scoped `launch` tool (gated by `launch.enabled`) for managing shared long-running services and debuggers, featuring readiness probes, bounded logs, PTY input, restart policies, and automatic teardown.
|
||||
- Added support for `detached` launches, allowing standalone services to survive broker shutdowns and reconnect to subsequent sessions.
|
||||
|
||||
### Changed
|
||||
|
||||
- Refined the downshift planning instructions to require a super-detailed todo list with one item per concrete step, each specifying its target and verification method
|
||||
- Updated tangential agent forks to ignore parent session history and focus exclusively on the new request
|
||||
- Hardened `/tan` fork isolation: the clone's inherited todo list is cleared at fork (parent todo reminders no longer drag the tan back onto the parent's task), the fork notice warns that the parent is concurrently editing the same working directory, and the notice is re-injected after each compaction so the fork boundary survives summarization
|
||||
- Added visual markers in the transcript for elided tool calls that have no corresponding result
|
||||
- Updated status event log to prioritize the most recent entries in the display window
|
||||
- Updated the snapcompact shape preview transcript to use the compact scope format shown to models during compaction.
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed the `--downshift-boomerang` feature and its associated configuration setting
|
||||
- Removed the unreliable Bing and Yahoo HTML-scraping web search providers
|
||||
- Updated JSON logs (`--mode json`) to include provider payloads in auto-compaction events.
|
||||
- Updated tangential agent forks (`/tan`) to ignore parent session history and focus exclusively on the new request, hardening isolation with cleared todo lists and concurrent editing warnings.
|
||||
- Added visual markers in the transcript for elided tool calls that have no corresponding result.
|
||||
- Updated the status event log to prioritize the most recent entries in the display window.
|
||||
- Upgraded `@agentclientprotocol/sdk` to version 1.2.1.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `/tan` and `/fork` clones cold-missing the provider prompt cache: the per-turn supersede/useless-result prune rewrote the live context without persisting it, so file-based forks and resume rebuilt a divergent (un-pruned) prefix and re-wrote the entire cache
|
||||
- Fixed `/tan` pinning the clone's prompt-cache key to the parent's session id instead of the parent's effective cache key, dropping shard affinity when the parent was itself a fork or tan
|
||||
- Fixed inconsistent history rendering when toggling the display setting for compacted items
|
||||
- Fixed configured `retry.fallbackChains` never engaging on non-retryable provider errors (e.g. "Cloud Code Assist API returned an empty response"): a hard error on a model covered by a fallback chain now switches to the next candidate instead of failing the turn, while still never backoff-retrying the failing model itself
|
||||
- Fixed transcript rebuilds (compaction, `/compact`, and toggling history display) repainting content below stale scrollback when collapsing history; rebuilds now correctly clear the scrollback buffer when history is collapsed
|
||||
- Improved auto-compaction to automatically drop images and elide content when context is tight, and added persistent warning badges to the compaction divider when manual intervention is required
|
||||
- Fixed backgrounded Bash blocks continuing to repaint with live and final job output; they now freeze with a compact job notice while completion is delivered separately
|
||||
- Fixed the downshift plan nudge silently ending the run with no code written when the model answered with a text-only reply (no tool call): the agent loop treats a tool-call-free turn as a natural stop and never prompts again, which the nudge's own "write the plan in your next reply" instruction makes common. The nudge now explicitly tells the model this is a checkpoint, not a final answer, and the session forces one more turn whenever a post-nudge reply lands with zero tool calls
|
||||
- Fixed launch tool rendering stacking a stale pending header over a bare `✓ Launch` line and raw text: the tool now uses a merged registry renderer with one per-op status header (op, target, `state · pid · uptime` meta), stripped log cursor suffixes, capped collapsed log/list previews, and a launch tool glyph
|
||||
- Fixed confusing launch start/wait results when readiness timed out with the log pattern already matched (readiness needs log AND port): the result printed a contradictory `Ready: <match>` next to `Readiness timed out` without naming the failing condition. Daemon snapshots now carry the unmet conditions (`readyPending`), and start/wait results state exactly what never happened (e.g. `port 3100 on 127.0.0.1 never accepted connections`); the TUI shows a `waiting on port` badge on starting daemons
|
||||
- Fixed the in-process `stat` builtin mangling BSD-style invocations like `stat -f "%Sm %N" file` (macOS muscle memory): GNU `-f` means `--file-system`, so the format string was treated as a file operand — printing filesystem info for the real operands and erroring with `cannot read file system information for '%Sm %N'`. A `-f` whose format value contains `%` is now detected as BSD syntax and translated to the GNU equivalent (`%Sm`→`%y`, `%N`→`%n`, `%z`→`%s`, epoch/`S`-form times, owner/group/permission and `H`/`L` sub-field directives, `-L`/`-n`/`-q`/`-F` flag clusters, with `%n`/`%t` as literal newline/tab); directives with no GNU counterpart fail with a clear `unsupported BSD format directive` error
|
||||
- Fixed the remaining GNU-flavored shell builtins that broke under macOS/BSD muscle memory, using the same unambiguous-detection approach as the `stat` fix (only invocations that are invalid or nonsensical under GNU semantics are reinterpreted; unsupported BSD forms fail loudly instead of producing wrong output): `date -r <epoch>` formats the epoch when no such file exists (GNU `-r FILE` mtime preserved), signed `date -v±N<unit>` adjustments translate to `-d` relative dates and `-j` is accepted (`-j -f` strptime parse mode and field-set `-v` error clearly); `sed -i '' 's/…/…/' file` drops the BSD empty backup-suffix token instead of treating it as the script; `mktemp -t prefix` without X's creates `$TMPDIR/prefix.XXXXXXXXXX` (the GNU `too few X's` error path); `tail -r` reverses input by delegating to `tac` (with `-n`/`-c`/`-f` combinations erroring clearly); `find -E` maps to `-regextype posix-extended` ahead of the expression; `base64 -D` decodes as an alias of `-d`; and `ln -sfh` works via a `-h` alias of `--no-dereference` (clap's `-h` help short is dropped to match real GNU/BSD ln; `--help` unchanged)
|
||||
- Fixed terminal scrollback duplication issues with expanded streaming edit previews (Ctrl+O) by using a viewport-sized tail window.
|
||||
- Fixed custom model role resolution and alias parsing, ensuring canonical role selectors (`@role`) and thinking suffixes resolve correctly across all configuration surfaces.
|
||||
- Fixed quadratic growth in JSON logs by eliding redundant message snapshots and payloads.
|
||||
- Fixed prompt cache misses and incorrect cache key pinning for `/tan` and `/fork` clones.
|
||||
- Fixed inconsistent history rendering and scrollback repainting when toggling the display setting for compacted items.
|
||||
- Fixed `retry.fallbackChains` failing to engage on non-retryable provider errors, ensuring the agent correctly falls back to the next candidate model.
|
||||
- Improved auto-compaction to automatically drop images and elide content when context is tight, and added persistent warning badges when manual intervention is required.
|
||||
- Fixed backgrounded Bash blocks continuing to repaint with live output; they now freeze with a compact job notice while completion is delivered separately.
|
||||
- Fixed rendering, status display, and PTY control sequence formatting issues in the `launch` tool.
|
||||
- Fixed in-process shell builtins (including `stat`, `date`, `sed`, `mktemp`, `tail`, `find`, `base64`, and `ln`) to correctly detect and translate macOS/BSD-style arguments and flags, preventing failures caused by GNU-only assumptions.
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed the `--prewalk-boomerang` feature and its associated configuration setting.
|
||||
- Removed the unreliable Bing and Yahoo HTML-scraping web search providers.
|
||||
|
||||
## [16.4.8] - 2026-07-12
|
||||
|
||||
### Added
|
||||
|
||||
- Added a predicate form to the browser run's `wait()` helper: `wait(fn, { timeout?, interval? })` polls the function (sync or async) until truthy and resolves with that value, failing with a named timeout error (deadline clamped under the cell budget so it always beats the opaque whole-cell timeout) instead of Bun's `sleep expects a number` or a whole-cell stall from in-page polling Promises; both `wait` forms now register in the stall diagnosis of cell timeouts
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "@oh-my-pi/pi-coding-agent",
|
||||
"version": "16.4.8",
|
||||
"version": "16.5.0",
|
||||
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
||||
"homepage": "https://omp.sh",
|
||||
"author": "Can Boluk",
|
||||
|
||||
@@ -27,9 +27,9 @@ export interface Args {
|
||||
smol?: string;
|
||||
slow?: string;
|
||||
plan?: string;
|
||||
downshift?: boolean;
|
||||
noDownshift?: boolean;
|
||||
downshiftInto?: string;
|
||||
prewalk?: boolean;
|
||||
noPrewalk?: boolean;
|
||||
prewalkInto?: string;
|
||||
planYolo?: boolean;
|
||||
planYoloInto?: string;
|
||||
maxTime?: number;
|
||||
@@ -236,10 +236,10 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { ty
|
||||
result.hideThinking = true;
|
||||
} else if (arg === "--advisor") {
|
||||
result.advisor = true;
|
||||
} else if (arg === "--downshift") {
|
||||
result.downshift = true;
|
||||
} else if (arg === "--no-downshift") {
|
||||
result.noDownshift = true;
|
||||
} else if (arg === "--prewalk") {
|
||||
result.prewalk = true;
|
||||
} else if (arg === "--no-prewalk") {
|
||||
result.noPrewalk = true;
|
||||
} else if (arg === "--plan-yolo") {
|
||||
result.planYolo = true;
|
||||
} else if (arg === "--print" || arg === "-p") {
|
||||
|
||||
@@ -121,8 +121,8 @@ export const STRING_SETTERS: Record<string, StringSetter> = {
|
||||
"--plan": (result, value) => {
|
||||
result.plan = value;
|
||||
},
|
||||
"--downshift-into": (result, value) => {
|
||||
result.downshiftInto = value;
|
||||
"--prewalk-into": (result, value) => {
|
||||
result.prewalkInto = value;
|
||||
},
|
||||
"--plan-yolo-into": (result, value) => {
|
||||
result.planYoloInto = value;
|
||||
@@ -281,8 +281,8 @@ export const VALUELESS_FLAGS: ReadonlySet<string> = new Set([
|
||||
"--no-pty",
|
||||
"--hide-thinking",
|
||||
"--advisor",
|
||||
"--downshift",
|
||||
"--no-downshift",
|
||||
"--prewalk",
|
||||
"--no-prewalk",
|
||||
"--plan-yolo",
|
||||
"--print",
|
||||
"--print-thoughts",
|
||||
|
||||
@@ -102,24 +102,33 @@ export const shellFixtures: Record<string, GalleryFixture> = {
|
||||
launch_logs: {
|
||||
label: "Launch",
|
||||
renderer: "launch",
|
||||
args: { op: "logs", name: "web", lines: 100, follow: true, cursor: 1842, timeout: 30 },
|
||||
args: { op: "logs", name: "comp-debug", lines: 100, follow: true, cursor: 233_512, timeout: 30 },
|
||||
result: {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: [
|
||||
"$ bun run dev",
|
||||
" VITE v6.0.3 ready in 312 ms",
|
||||
"",
|
||||
" ➜ Local: http://localhost:5173/",
|
||||
" ➜ Network: use --host to expose",
|
||||
"12:04:11 [vite] hmr update /src/App.tsx",
|
||||
"12:04:15 [vite] hmr update /src/components/Chart.tsx",
|
||||
"[web: running; cursor=2210]",
|
||||
"Breakpoint 1: 3 locations.",
|
||||
"(lldb) run",
|
||||
"Process 726 launched: '/tmp/compiler'",
|
||||
"frame #0: 0x0000000100012f80 compiler`parse_expression",
|
||||
"[comp-debug: ready; cursor=233797]",
|
||||
].join("\n"),
|
||||
},
|
||||
],
|
||||
details: { op: "logs", cursor: 2210, timedOut: false, state: "running" },
|
||||
details: {
|
||||
op: "logs",
|
||||
cursor: 233_797,
|
||||
timedOut: false,
|
||||
state: "ready",
|
||||
terminalRows: [
|
||||
"\x1b[0mBreakpoint 1: 3 locations.",
|
||||
"\x1b[0m(lldb) run",
|
||||
"\x1b[0mProcess 726 launched: '/tmp/compiler'",
|
||||
"\x1b[0mframe #0: 0x0000000100012f80 compiler`parse_expression",
|
||||
"\x1b[0m\x1b[1;38;5;2m(lldb)\x1b[0m ",
|
||||
],
|
||||
},
|
||||
},
|
||||
errorResult: {
|
||||
content: [{ type: "text", text: "No daemon named web" }],
|
||||
|
||||
@@ -34,15 +34,15 @@ export default class Index extends Command {
|
||||
plan: Flags.string({
|
||||
description: "Plan model for architectural planning (or PI_PLAN_MODEL env)",
|
||||
}),
|
||||
downshift: Flags.boolean({
|
||||
prewalk: Flags.boolean({
|
||||
description:
|
||||
"Switch from the active model to a fast/cheap model at the first edit/write after the plan's todo list exists (default off; see downshift.enabled)",
|
||||
"Switch from the active model to a fast/cheap model at the first edit/write after the plan's todo list exists (default off; see prewalk.enabled)",
|
||||
}),
|
||||
"no-downshift": Flags.boolean({
|
||||
description: "Disable downshift even if downshift.enabled is set",
|
||||
"no-prewalk": Flags.boolean({
|
||||
description: "Disable prewalk even if prewalk.enabled is set",
|
||||
}),
|
||||
"downshift-into": Flags.string({
|
||||
description: 'Target model for downshift (default the "smol" role)',
|
||||
"prewalk-into": Flags.string({
|
||||
description: 'Target model for prewalk (default the "smol" role)',
|
||||
}),
|
||||
"plan-yolo": Flags.boolean({
|
||||
description:
|
||||
|
||||
@@ -37,7 +37,14 @@ import {
|
||||
resolveThinkingLevelForModel,
|
||||
} from "../thinking";
|
||||
import { isAuthenticated, kNoAuth, type ModelRegistry } from "./model-registry";
|
||||
import { MODEL_ROLE_IDS, type ModelRole } from "./model-roles";
|
||||
import {
|
||||
DEFAULT_MODEL_ROLE_ALIAS,
|
||||
formatModelRoleAlias,
|
||||
LEGACY_MODEL_ROLE_ALIAS_PREFIX,
|
||||
MODEL_ROLE_ALIAS_PREFIX,
|
||||
MODEL_ROLE_IDS,
|
||||
type ModelRole,
|
||||
} from "./model-roles";
|
||||
import type { Settings } from "./settings";
|
||||
|
||||
function isKnownProvider(provider: string): provider is KnownProvider {
|
||||
@@ -109,8 +116,7 @@ function parseThinkingSuffix(value: string, options?: ThinkingSuffixOptions): Co
|
||||
* level / `:auto` sentinel); `base` then has the suffix stripped. Otherwise
|
||||
* `base` is the input.
|
||||
* `minColonIndex` requires the colon to appear strictly after that index —
|
||||
* role-alias callers pass `PREFIX_MODEL_ROLE.length` so the base is at least
|
||||
* as long as the `pi/` prefix.
|
||||
* role-alias callers pass the matched alias prefix length.
|
||||
*/
|
||||
function splitThinkingSuffix(
|
||||
pattern: string,
|
||||
@@ -850,17 +856,32 @@ export function parseModelPattern(
|
||||
);
|
||||
}
|
||||
|
||||
const PREFIX_MODEL_ROLE = "pi/";
|
||||
const DEFAULT_MODEL_ROLE = "default";
|
||||
const MODEL_ROLE_ALIAS_PREFIXES = [MODEL_ROLE_ALIAS_PREFIX, LEGACY_MODEL_ROLE_ALIAS_PREFIX];
|
||||
|
||||
function getModelRoleAlias(value: string): ModelRole | undefined {
|
||||
function isModelRole(role: string): role is ModelRole {
|
||||
return (MODEL_ROLE_IDS as string[]).includes(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum colon index for splitting a `:<level>` suffix off a role alias, or
|
||||
* `undefined` when `value` is not role-alias shaped. Doubles as the slice
|
||||
* offset of the role name for prefixed aliases (`@role`, `pi/role`); the bare
|
||||
* `*` default alias returns 0 because its colon sits immediately after the
|
||||
* one-character token (`*:xhigh`).
|
||||
*/
|
||||
function modelRoleAliasPrefixLength(value: string): number | undefined {
|
||||
if (value === DEFAULT_MODEL_ROLE_ALIAS || value.startsWith(`${DEFAULT_MODEL_ROLE_ALIAS}:`)) return 0;
|
||||
return MODEL_ROLE_ALIAS_PREFIXES.find(prefix => value.startsWith(prefix))?.length;
|
||||
}
|
||||
|
||||
function getModelRoleAlias(value: string, settings?: Settings): string | undefined {
|
||||
const normalized = value.trim();
|
||||
if (!normalized.startsWith(PREFIX_MODEL_ROLE)) return undefined;
|
||||
const prefixLength = modelRoleAliasPrefixLength(normalized);
|
||||
if (prefixLength === undefined) return undefined;
|
||||
|
||||
const candidate = normalized.slice(PREFIX_MODEL_ROLE.length);
|
||||
for (const role of MODEL_ROLE_IDS) {
|
||||
if (candidate === role) return role;
|
||||
}
|
||||
const candidate = normalized === DEFAULT_MODEL_ROLE_ALIAS ? DEFAULT_MODEL_ROLE : normalized.slice(prefixLength);
|
||||
if (isModelRole(candidate) || settings?.getModelRole(candidate) !== undefined) return candidate;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -871,7 +892,14 @@ function normalizeModelPatternList(value: string | string[] | undefined): string
|
||||
}
|
||||
|
||||
function isSessionInheritedAgentPattern(value: string): boolean {
|
||||
return value === DEFAULT_MODEL_ROLE || value === `${PREFIX_MODEL_ROLE}${DEFAULT_MODEL_ROLE}` || value === "pi/task";
|
||||
return (
|
||||
value === DEFAULT_MODEL_ROLE ||
|
||||
value === formatModelRoleAlias(DEFAULT_MODEL_ROLE) ||
|
||||
value === DEFAULT_MODEL_ROLE_ALIAS ||
|
||||
value === `${LEGACY_MODEL_ROLE_ALIAS_PREFIX}${DEFAULT_MODEL_ROLE}` ||
|
||||
value === formatModelRoleAlias("task") ||
|
||||
value === `${LEGACY_MODEL_ROLE_ALIAS_PREFIX}task`
|
||||
);
|
||||
}
|
||||
|
||||
function shouldInheritDefaultBeforePriority(role: ModelRole): boolean {
|
||||
@@ -904,7 +932,7 @@ function resolveDefaultInheritedPatterns(
|
||||
configuredDefault: string | undefined,
|
||||
roleDefaults: string[],
|
||||
settings: Settings | undefined,
|
||||
visited: Set<ModelRole>,
|
||||
visited: Set<string>,
|
||||
): string[] {
|
||||
if (!shouldInheritDefaultBeforePriority(role) || !configuredDefault) return [];
|
||||
|
||||
@@ -912,13 +940,12 @@ function resolveDefaultInheritedPatterns(
|
||||
for (const pattern of normalizeModelPatternList(configuredDefault)) {
|
||||
const { base: aliasCandidate, level: thinkingLevel } = splitThinkingSuffix(
|
||||
pattern,
|
||||
PREFIX_MODEL_ROLE.length,
|
||||
modelRoleAliasPrefixLength(pattern) ?? LEGACY_MODEL_ROLE_ALIAS_PREFIX.length,
|
||||
MAX_THINKING_SUFFIX_OPTIONS,
|
||||
);
|
||||
const aliasRole = getModelRoleAlias(aliasCandidate);
|
||||
const aliasRole = getModelRoleAlias(aliasCandidate, settings);
|
||||
if (aliasRole === role) {
|
||||
// Self-alias (e.g. modelRoles.default = "pi/smol") would loop back to the
|
||||
// same unset role; collapse straight to the built-in priority chain.
|
||||
// Self-alias (e.g. modelRoles.default = "@smol") would loop back to the
|
||||
resolved.push(
|
||||
...(thinkingLevel
|
||||
? roleDefaults.map(defaultPattern => `${defaultPattern}:${thinkingLevel}`)
|
||||
@@ -927,8 +954,7 @@ function resolveDefaultInheritedPatterns(
|
||||
continue;
|
||||
}
|
||||
if (aliasRole && !visited.has(aliasRole)) {
|
||||
// Cross-role alias (e.g. modelRoles.default = "pi/slow"): resolve the
|
||||
// target role's patterns now so downstream one-layer expanders see
|
||||
// Cross-role alias (e.g. modelRoles.default = "@slow"): resolve the
|
||||
// concrete model patterns instead of another role alias.
|
||||
const recursed = resolveConfiguredRolePattern(pattern, settings, new Set(visited));
|
||||
if (recursed && recursed.length > 0) {
|
||||
@@ -944,27 +970,29 @@ function resolveDefaultInheritedPatterns(
|
||||
function resolveConfiguredRolePattern(
|
||||
value: string,
|
||||
settings?: Settings,
|
||||
visited: Set<ModelRole> = new Set(),
|
||||
visited: Set<string> = new Set(),
|
||||
): string[] | undefined {
|
||||
const normalized = value.trim();
|
||||
if (!normalized) return undefined;
|
||||
|
||||
const { base: aliasCandidate, level: thinkingLevel } = splitThinkingSuffix(
|
||||
normalized,
|
||||
PREFIX_MODEL_ROLE.length,
|
||||
modelRoleAliasPrefixLength(normalized) ?? LEGACY_MODEL_ROLE_ALIAS_PREFIX.length,
|
||||
MAX_THINKING_SUFFIX_OPTIONS,
|
||||
);
|
||||
const role = getModelRoleAlias(aliasCandidate);
|
||||
const role = getModelRoleAlias(aliasCandidate, settings);
|
||||
if (!role) return [normalized];
|
||||
if (visited.has(role)) return undefined;
|
||||
visited.add(role);
|
||||
|
||||
const configured = settings?.getModelRole(role)?.trim();
|
||||
const configuredDefault = settings?.getModelRole(DEFAULT_MODEL_ROLE)?.trim();
|
||||
const roleDefaults = rolePriorityDefaults(role);
|
||||
const roleDefaults = isModelRole(role) ? rolePriorityDefaults(role) : [];
|
||||
const resolved = configured
|
||||
? normalizeModelPatternList(configured)
|
||||
: resolveDefaultInheritedPatterns(role, configuredDefault, roleDefaults, settings, visited);
|
||||
: isModelRole(role)
|
||||
? resolveDefaultInheritedPatterns(role, configuredDefault, roleDefaults, settings, visited)
|
||||
: roleDefaults;
|
||||
if (resolved.length === 0) {
|
||||
resolved.push(...roleDefaults);
|
||||
}
|
||||
@@ -976,7 +1004,7 @@ function resolveConfiguredRolePattern(
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a role alias like "pi/smol" to the configured model string.
|
||||
* Expand a role alias like "@smol" to the configured model string.
|
||||
*/
|
||||
export function expandRoleAlias(value: string, settings?: Settings): string {
|
||||
const normalized = value.trim();
|
||||
@@ -1014,8 +1042,13 @@ export function resolveAgentModelPatterns(options: AgentModelPatternResolutionOp
|
||||
const singleAgentPattern = normalizedAgentPatterns.length === 1 ? normalizedAgentPatterns[0] : undefined;
|
||||
const agentInheritsSessionModel = singleAgentPattern ? isSessionInheritedAgentPattern(singleAgentPattern) : false;
|
||||
if (configuredAgentPatterns.length > 0) {
|
||||
if (
|
||||
singleAgentPattern === formatModelRoleAlias("task") ||
|
||||
singleAgentPattern === `${LEGACY_MODEL_ROLE_ALIAS_PREFIX}task`
|
||||
) {
|
||||
return configuredAgentPatterns;
|
||||
}
|
||||
if (!agentInheritsSessionModel) return configuredAgentPatterns;
|
||||
if (singleAgentPattern === "pi/task") return configuredAgentPatterns;
|
||||
}
|
||||
|
||||
const fallback =
|
||||
@@ -1102,12 +1135,16 @@ export function extractExplicitThinkingSelector(
|
||||
let current = normalized;
|
||||
while (!visited.has(current)) {
|
||||
visited.add(current);
|
||||
const strictSelector = splitThinkingSuffix(current, PREFIX_MODEL_ROLE.length).level;
|
||||
const rolePrefixLength = modelRoleAliasPrefixLength(current) ?? LEGACY_MODEL_ROLE_ALIAS_PREFIX.length;
|
||||
const strictSelector = splitThinkingSuffix(current, rolePrefixLength).level;
|
||||
if (strictSelector) {
|
||||
return strictSelector;
|
||||
}
|
||||
const maxSelector = splitThinkingSuffix(current, PREFIX_MODEL_ROLE.length, MAX_THINKING_SUFFIX_OPTIONS).level;
|
||||
if (maxSelector && (current.startsWith(PREFIX_MODEL_ROLE) || !isLiteralModelSelector(current, options))) {
|
||||
const maxSelector = splitThinkingSuffix(current, rolePrefixLength, MAX_THINKING_SUFFIX_OPTIONS).level;
|
||||
if (
|
||||
maxSelector &&
|
||||
(modelRoleAliasPrefixLength(current) !== undefined || !isLiteralModelSelector(current, options))
|
||||
) {
|
||||
return maxSelector;
|
||||
}
|
||||
const expanded = expandRoleAlias(current, settings).trim();
|
||||
@@ -1278,7 +1315,7 @@ export function resolveAdvisorRoleSelection(
|
||||
settings: Settings,
|
||||
availableModels: Model<Api>[],
|
||||
): { model: Model<Api>; thinkingLevel?: ConfiguredThinkingLevel } | undefined {
|
||||
const resolved = resolveModelRoleValue(`${PREFIX_MODEL_ROLE}advisor`, availableModels, {
|
||||
const resolved = resolveModelRoleValue(formatModelRoleAlias("advisor"), availableModels, {
|
||||
settings,
|
||||
matchPreferences: getModelMatchPreferences(settings),
|
||||
});
|
||||
@@ -1300,6 +1337,7 @@ export async function resolveModelScope(
|
||||
patterns: string[],
|
||||
modelRegistry: Pick<ModelRegistry, "getAvailable">,
|
||||
preferences?: ModelMatchPreferences,
|
||||
settings?: Settings,
|
||||
): Promise<ScopedModel[]> {
|
||||
const availableModels = modelRegistry.getAvailable();
|
||||
const context = buildPreferenceContext(availableModels, preferences);
|
||||
@@ -1337,6 +1375,25 @@ export async function resolveModelScope(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Role aliases (`@smol`, `pi/slow`) resolve to the role's single concrete
|
||||
// model — not its whole fallback chain — so a role contributes one scope
|
||||
// entry exactly like `--model` would pick. (Bare `*` stays a match-all
|
||||
// glob above; scope semantics, not the default-role alias.)
|
||||
if (settings && modelRoleAliasPrefixLength(pattern) !== undefined) {
|
||||
const resolved = resolveModelRoleValue(pattern, availableModels, { settings, matchPreferences: preferences });
|
||||
if (resolved.warning) logger.warn(resolved.warning);
|
||||
if (!resolved.model) {
|
||||
logger.warn(`No models match pattern "${pattern}"`);
|
||||
continue;
|
||||
}
|
||||
if (resolved.thinkingLevel === AUTO_THINKING) {
|
||||
addScopedModel(resolved.model, undefined, false);
|
||||
} else {
|
||||
addScopedModel(resolved.model, resolved.thinkingLevel, resolved.explicitThinkingLevel);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const { model, thinkingLevel, warning, explicitThinkingLevel } = parseModelPatternWithContext(
|
||||
pattern,
|
||||
availableModels,
|
||||
@@ -1386,7 +1443,7 @@ export async function resolveAllowedModels(
|
||||
if (!patterns || patterns.length === 0) {
|
||||
return available;
|
||||
}
|
||||
const scoped = await resolveModelScope(patterns, modelRegistry, preferences);
|
||||
const scoped = await resolveModelScope(patterns, modelRegistry, preferences, settings);
|
||||
if (scoped.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -1415,6 +1472,7 @@ export async function resolveAllowedModels(
|
||||
export function filterAvailableModelsByEnabledPatterns(
|
||||
available: Model<Api>[],
|
||||
patterns: readonly string[],
|
||||
settings?: Settings,
|
||||
): Model<Api>[] {
|
||||
if (patterns.length === 0) return available;
|
||||
|
||||
@@ -1432,6 +1490,13 @@ export function filterAvailableModelsByEnabledPatterns(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mirror resolveModelScope: role aliases resolve to the role's model.
|
||||
if (settings && modelRoleAliasPrefixLength(pattern) !== undefined) {
|
||||
const { model } = resolveModelRoleValue(pattern, available, { settings });
|
||||
if (model) addAllowed(model);
|
||||
continue;
|
||||
}
|
||||
|
||||
const { model } = parseModelPatternWithContext(pattern, available, context);
|
||||
if (model) {
|
||||
addAllowed(model);
|
||||
@@ -1456,9 +1521,10 @@ export function resolveCliModel(options: {
|
||||
cliProvider?: string;
|
||||
cliModel?: string;
|
||||
modelRegistry: CliModelRegistry;
|
||||
settings?: Settings;
|
||||
preferences?: ModelMatchPreferences;
|
||||
}): ResolveCliModelResult {
|
||||
const { cliProvider, cliModel, modelRegistry, preferences } = options;
|
||||
const { cliProvider, cliModel, modelRegistry, settings, preferences } = options;
|
||||
|
||||
if (!cliModel) {
|
||||
return { model: undefined, selector: undefined, warning: undefined, error: undefined };
|
||||
@@ -1474,6 +1540,19 @@ export function resolveCliModel(options: {
|
||||
};
|
||||
}
|
||||
|
||||
if (!cliProvider && modelRoleAliasPrefixLength(cliModel) !== undefined) {
|
||||
const resolved = resolveModelRoleValue(cliModel, availableModels, { settings, matchPreferences: preferences });
|
||||
if (resolved.model) {
|
||||
return {
|
||||
model: resolved.model,
|
||||
selector: formatModelString(resolved.model),
|
||||
thinkingLevel: resolved.thinkingLevel,
|
||||
warning: resolved.warning,
|
||||
error: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const providerMap = new Map<string, string>();
|
||||
for (const model of availableModels) {
|
||||
providerMap.set(model.provider.toLowerCase(), model.provider);
|
||||
|
||||
@@ -5,6 +5,20 @@
|
||||
import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
|
||||
import type { Settings } from "./settings";
|
||||
|
||||
/** Canonical prefix for a configured model role selector. */
|
||||
export const MODEL_ROLE_ALIAS_PREFIX = "@";
|
||||
|
||||
/** Legacy prefix accepted for backwards-compatible role selectors. */
|
||||
export const LEGACY_MODEL_ROLE_ALIAS_PREFIX = "pi/";
|
||||
|
||||
/** Shorthand selector for the default model role. */
|
||||
export const DEFAULT_MODEL_ROLE_ALIAS = "*";
|
||||
|
||||
/** Format a model role as its canonical selector. */
|
||||
export function formatModelRoleAlias(role: string): string {
|
||||
return `${MODEL_ROLE_ALIAS_PREFIX}${role}`;
|
||||
}
|
||||
|
||||
export type ModelRole =
|
||||
| "default"
|
||||
| "smol"
|
||||
|
||||
@@ -117,7 +117,7 @@ export const TAB_METADATA: Record<SettingTab, { label: string; icon: `tab.${stri
|
||||
*/
|
||||
export const TAB_GROUPS: Record<SettingTab, readonly string[]> = {
|
||||
appearance: ["Theme", "Status Line", "Display", "Images"],
|
||||
model: ["Thinking", "Sampling", "Prompt", "Retry & Fallback", "Advisor", "Downshift", "Vision"],
|
||||
model: ["Thinking", "Sampling", "Prompt", "Retry & Fallback", "Advisor", "Prewalk", "Vision"],
|
||||
interaction: [
|
||||
"Input",
|
||||
"Approvals",
|
||||
@@ -422,15 +422,15 @@ export const SETTINGS_SCHEMA = {
|
||||
"Pair a second model (assigned to the 'advisor' role) that passively reviews each turn and injects notes.",
|
||||
},
|
||||
},
|
||||
"downshift.enabled": {
|
||||
"prewalk.enabled": {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
ui: {
|
||||
tab: "model",
|
||||
group: "Downshift",
|
||||
label: "Enable Downshift",
|
||||
group: "Prewalk",
|
||||
label: "Enable Prewalk",
|
||||
description:
|
||||
"Start on the active model, then switch to a fast/cheap model (default the 'smol' role) at the first edit/write after the plan nudge's todo list exists — the strong model plans, commits the todos, and starts the implementation before handing off. Overridable per session with --downshift / --no-downshift.",
|
||||
"Start on the active model, then switch to a fast/cheap model (default the 'smol' role) at the first edit/write after the plan nudge's todo list exists — the strong model plans, commits the todos, and starts the implementation before handing off. Overridable per session with --prewalk / --no-prewalk.",
|
||||
},
|
||||
},
|
||||
"advisor.subagents": {
|
||||
@@ -897,6 +897,17 @@ export const SETTINGS_SCHEMA = {
|
||||
description: "Remove the 1-character horizontal padding from the left and right of the terminal output",
|
||||
},
|
||||
},
|
||||
"tui.scrollbackRebuild": {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
ui: {
|
||||
tab: "appearance",
|
||||
group: "Display",
|
||||
label: "Rewrite Scrollback",
|
||||
description:
|
||||
"Erase and replay terminal scrollback when a block's final form replaces its live preview. When off (default), stale preview copies remain in history and the final content is appended below.",
|
||||
},
|
||||
},
|
||||
|
||||
"display.shimmer": {
|
||||
type: "enum",
|
||||
@@ -2639,14 +2650,14 @@ export const SETTINGS_SCHEMA = {
|
||||
group: "Mnemopi",
|
||||
label: "Mnemopi LLM Mode",
|
||||
description:
|
||||
"Use no LLM, the online tiny model (the TINY role from /models, else pi/smol), or a remote OpenAI-compatible endpoint",
|
||||
"Use no LLM, the online tiny model (the TINY role from /models, else @smol), or a remote OpenAI-compatible endpoint",
|
||||
condition: "mnemopiActive",
|
||||
options: [
|
||||
{ value: "none", label: "None", description: "Disable Mnemopi LLM-backed extraction" },
|
||||
{
|
||||
value: "smol",
|
||||
label: "Online (tiny)",
|
||||
description: "Use the online tiny model (the TINY role from /models, else pi/smol)",
|
||||
description: "Use the online tiny model (the TINY role from /models, else @smol)",
|
||||
},
|
||||
{ value: "remote", label: "Remote", description: "Use the Mnemopi remote LLM settings below" },
|
||||
],
|
||||
@@ -4600,7 +4611,7 @@ export const SETTINGS_SCHEMA = {
|
||||
group: "Tiny Model",
|
||||
label: "Tiny Model",
|
||||
description:
|
||||
"Session-title model: online (the TINY role from /models, else pi/smol) by default, or a local on-device model",
|
||||
"Session-title model: online (the TINY role from /models, else @smol) by default, or a local on-device model",
|
||||
options: TINY_TITLE_MODEL_OPTIONS,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -423,22 +423,25 @@ function formatStreamingDiff(
|
||||
cache?: RenderedStringCache,
|
||||
): string {
|
||||
if (!diff) return "";
|
||||
// Clamp the collapsed tail to the viewport so a tall or fast-growing diff
|
||||
// cannot outgrow the live window. Otherwise its mutating tail scrolls above
|
||||
// the native-scrollback commit boundary and the engine re-commits a fresh
|
||||
// snapshot every streamed frame, stacking duplicate "… more lines above"
|
||||
// previews in history. The budget is VISUAL rows (a long wrapped line counts
|
||||
// Clamp the tail to the viewport so a tall or fast-growing diff cannot
|
||||
// outgrow the live window. Otherwise its mutating rows scroll above the
|
||||
// native-scrollback commit boundary mid-stream and freeze into immutable
|
||||
// history as a stale preview snapshot; the finalize repair then recommits
|
||||
// the final render below it — a duplicated block on the tape. Collapsed
|
||||
// gets a short fixed tail; expanded widens it to the viewport-sized window,
|
||||
// never unbounded. The budget is VISUAL rows (a long wrapped line counts
|
||||
// for more than one) at the framed block's inner width (border only —
|
||||
// contentPaddingLeft is 0); only the visible suffix is syntax-colored, so the
|
||||
// cheap raw-line wrap walk keeps the per-chunk cost bounded. innerWidth/budget
|
||||
// are in the cache salt so a resize re-slices.
|
||||
// contentPaddingLeft is 0); only the visible suffix is syntax-colored, so
|
||||
// the cheap raw-line wrap walk keeps the per-chunk cost bounded.
|
||||
// innerWidth/budget are in the cache salt so a resize re-slices.
|
||||
const innerWidth = Math.max(1, width - 2);
|
||||
const budget = expanded ? Number.POSITIVE_INFINITY : Math.min(EDIT_STREAMING_PREVIEW_LINES, previewWindowRows());
|
||||
const budget = expanded ? previewWindowRows() : Math.min(EDIT_STREAMING_PREVIEW_LINES, previewWindowRows());
|
||||
let text = cachedRenderedString(cache, uiTheme, expanded, `${rawPath}:${innerWidth}:${budget}`, diff, () => {
|
||||
// "Cursor" tail window: pin the last rows to the bottom so freshly streamed
|
||||
// changes stay on screen. The whole-file diff is recomputed every chunk and
|
||||
// its Myers alignment is not monotonic in payload length, so a hunk-aware
|
||||
// window stutters as rows move between hunks. Expanded lifts the cap.
|
||||
// window stutters as rows move between hunks. Expanded widens the window
|
||||
// to the viewport; the full diff appears once the result finalizes.
|
||||
const allLines = diff.replace(/\n+$/u, "").split("\n");
|
||||
let visualUsed = 0;
|
||||
let cut = allLines.length;
|
||||
|
||||
@@ -28,7 +28,7 @@ const taskAgent = {
|
||||
systemPrompt: "Run the task.",
|
||||
source: "bundled",
|
||||
spawns: "*",
|
||||
model: ["pi/task"],
|
||||
model: ["@task"],
|
||||
} satisfies AgentDefinition;
|
||||
|
||||
const reviewerAgent = {
|
||||
@@ -36,7 +36,7 @@ const reviewerAgent = {
|
||||
description: "Reviewer agent",
|
||||
systemPrompt: "Review the task.",
|
||||
source: "bundled",
|
||||
model: ["pi/smol"],
|
||||
model: ["@smol"],
|
||||
} satisfies AgentDefinition;
|
||||
|
||||
interface SessionOptions {
|
||||
|
||||
@@ -193,7 +193,7 @@ describe("runEvalCompletion", () => {
|
||||
expect(resolved).toEqual(["p/smol", "p/default", "p/slow"]);
|
||||
});
|
||||
|
||||
it("prefers the session active model for the default tier, falling back to pi/default", async () => {
|
||||
it("prefers the session active model for the default tier, falling back to @default", async () => {
|
||||
const spy = vi.spyOn(ai, "completeSimple").mockResolvedValue(assistant({ text: "ok" }));
|
||||
const session = makeSession({ available: [SMOL, DEFAULT, SLOW], activeModel: "p/slow" });
|
||||
|
||||
|
||||
@@ -37,9 +37,9 @@ const STRUCTURED_TOOL_NAME = "respond";
|
||||
type CompletionTier = "smol" | "default" | "slow";
|
||||
|
||||
const TIER_TO_PATTERN: Record<CompletionTier, string> = {
|
||||
smol: "pi/smol",
|
||||
default: "pi/default",
|
||||
slow: "pi/slow",
|
||||
smol: "@smol",
|
||||
default: "@default",
|
||||
slow: "@slow",
|
||||
};
|
||||
|
||||
const completionArgsSchema = type({
|
||||
@@ -62,7 +62,7 @@ export interface EvalCompletionResult {
|
||||
|
||||
/**
|
||||
* Resolve a tier to a concrete {@link Model}. `default` prefers the session's
|
||||
* active model and falls back to the `pi/default` role; `smol`/`slow` resolve
|
||||
* active model and falls back to the `@default` role; `smol`/`slow` resolve
|
||||
* their respective role patterns. Returns `undefined` when nothing matches.
|
||||
*/
|
||||
function resolveTierModel(tier: CompletionTier, session: ToolSession): Model<Api> | undefined {
|
||||
|
||||
@@ -25,7 +25,7 @@ export function createExtensionModelQuery(
|
||||
return {
|
||||
list: () => modelRegistry.getAvailable(),
|
||||
current: () => getModel(),
|
||||
// resolveModelRoleValue expands a role alias (`pi/slow`) to its full configured
|
||||
// resolveModelRoleValue expands a role alias (`@slow`) to its full configured
|
||||
// priority list and tries each pattern — the same path core selection uses — so a
|
||||
// fallback model lower in the list still resolves. Plain model strings pass through
|
||||
// as a single pattern.
|
||||
|
||||
@@ -391,7 +391,7 @@ export interface ExtensionModelQuery {
|
||||
/** The current session model, if one is set. */
|
||||
current(): Model | undefined;
|
||||
/**
|
||||
* Resolve a model string (`provider/id`, bare id) or role alias (`pi/slow`, a
|
||||
* Resolve a model string (`provider/id`, bare id) or role alias (`@slow`, a
|
||||
* configured role) to a Model, using the same settings-backed aliases and match
|
||||
* preferences as core selection. Thinking/routing suffixes are accepted and resolved
|
||||
* to the base model (pass effort separately). Returns undefined when nothing matches.
|
||||
|
||||
@@ -4,13 +4,15 @@ import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { Process, type PtyRunResult, PtySession } from "@oh-my-pi/pi-natives";
|
||||
import { isEexist, isEnoent, logger, postmortem, sanitizeText } from "@oh-my-pi/pi-utils";
|
||||
import { truncateHead, truncateTail } from "../session/streaming-output";
|
||||
import { truncateHead, truncateHeadBytes, truncateTail, truncateTailBytes } from "../session/streaming-output";
|
||||
import { workerEnvFromParent } from "../subprocess/worker-client";
|
||||
import { daemonBrokerEndpoint } from "./paths";
|
||||
import { hasLiveDaemonProjectPresence } from "./presence";
|
||||
import {
|
||||
DAEMON_IDLE_GRACE_ENV,
|
||||
DAEMON_PROJECT_DIR_ENV,
|
||||
DAEMON_PTY_COLUMNS,
|
||||
DAEMON_PTY_ROWS,
|
||||
DAEMON_RUNTIME_DIR_ENV,
|
||||
type DaemonOperation,
|
||||
type DaemonReadySpec,
|
||||
@@ -74,6 +76,11 @@ interface BrokerLease {
|
||||
instanceId: string;
|
||||
}
|
||||
|
||||
interface DaemonLogRead {
|
||||
text: string;
|
||||
terminalText: string;
|
||||
}
|
||||
|
||||
function quoteShellArg(value: string): string {
|
||||
return `'${value.replaceAll("'", `'\\''`)}'`;
|
||||
}
|
||||
@@ -141,8 +148,7 @@ class DaemonLog {
|
||||
return new DaemonLog(logPath, previousPath, file, file.writer());
|
||||
}
|
||||
|
||||
append(raw: string): string {
|
||||
const text = sanitizeText(raw);
|
||||
append(text: string): string {
|
||||
if (text.length === 0 || this.#closed) return text;
|
||||
const bytes = Buffer.byteLength(text, "utf8");
|
||||
this.#queue = this.#queue.then(async () => {
|
||||
@@ -154,7 +160,7 @@ class DaemonLog {
|
||||
return text;
|
||||
}
|
||||
|
||||
async read(head: boolean, lines: number, grep?: string): Promise<string> {
|
||||
async read(head: boolean, lines: number, grep?: string): Promise<DaemonLogRead> {
|
||||
await this.#queue;
|
||||
await this.#writer.flush();
|
||||
return DaemonLog.readFiles(this.#path, this.#previousPath, head, lines, grep);
|
||||
@@ -167,19 +173,19 @@ class DaemonLog {
|
||||
await this.#writer.end();
|
||||
}
|
||||
|
||||
static async readDir(dir: string, head: boolean, lines: number, grep?: string): Promise<string> {
|
||||
return DaemonLog.readFiles(path.join(dir, LOG_FILE), path.join(dir, PREVIOUS_LOG_FILE), head, lines, grep);
|
||||
}
|
||||
|
||||
static async readFiles(
|
||||
logPath: string,
|
||||
previousPath: string,
|
||||
head: boolean,
|
||||
lines: number,
|
||||
grep?: string,
|
||||
): Promise<string> {
|
||||
): Promise<DaemonLogRead> {
|
||||
const [previous, current] = await Promise.all([fileTextSlice(previousPath, head), fileTextSlice(logPath, head)]);
|
||||
let text = sanitizeText(`${previous}${previous && current && !previous.endsWith("\n") ? "\n" : ""}${current}`);
|
||||
const combined = `${previous}${previous && current && !previous.endsWith("\n") ? "\n" : ""}${current}`;
|
||||
const terminalText = head
|
||||
? truncateHeadBytes(combined, LOG_READ_BYTES).text
|
||||
: truncateTailBytes(combined, LOG_READ_BYTES).text;
|
||||
let text = sanitizeText(terminalText);
|
||||
if (grep) {
|
||||
let pattern: RegExp;
|
||||
try {
|
||||
@@ -193,7 +199,10 @@ class DaemonLog {
|
||||
.join("\n");
|
||||
}
|
||||
const options = { maxLines: lines, maxBytes: 256 * 1024 };
|
||||
return head ? truncateHead(text, options).content : truncateTail(text, options).content;
|
||||
return {
|
||||
text: head ? truncateHead(text, options).content : truncateTail(text, options).content,
|
||||
terminalText,
|
||||
};
|
||||
}
|
||||
|
||||
async #rotate(): Promise<void> {
|
||||
@@ -527,8 +536,8 @@ class DaemonBroker {
|
||||
command,
|
||||
cwd: record.spec.cwd,
|
||||
env: workerEnvFromParent({ TERM: "xterm-256color", ...record.spec.env }),
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
cols: DAEMON_PTY_COLUMNS,
|
||||
rows: DAEMON_PTY_ROWS,
|
||||
shell,
|
||||
},
|
||||
(error, chunk) => {
|
||||
@@ -626,9 +635,10 @@ class DaemonBroker {
|
||||
|
||||
#onOutput(record: ManagedDaemon, generation: number, raw: string): void {
|
||||
if (generation !== record.generation) return;
|
||||
const text = record.log?.append(raw) ?? sanitizeText(raw);
|
||||
const output = raw.toWellFormed();
|
||||
const text = record.log?.append(output) ?? output;
|
||||
record.snapshot.outputBytes += Buffer.byteLength(text, "utf8");
|
||||
this.#trackOutput(record, generation, text);
|
||||
this.#trackOutput(record, generation, sanitizeText(text));
|
||||
}
|
||||
|
||||
async #readDetachedOutput(record: ManagedDaemon, generation: number): Promise<void> {
|
||||
@@ -753,13 +763,20 @@ class DaemonBroker {
|
||||
timedOut = !changed;
|
||||
}
|
||||
const lines = Math.max(1, Math.min(1_000, Math.floor(operation.lines)));
|
||||
const text = record.log
|
||||
const output = record.log
|
||||
? await record.log.read(operation.head, lines, operation.grep)
|
||||
: await DaemonLog.readDir(record.dir, operation.head, lines, operation.grep);
|
||||
: await DaemonLog.readFiles(
|
||||
path.join(record.dir, LOG_FILE),
|
||||
path.join(record.dir, PREVIOUS_LOG_FILE),
|
||||
operation.head,
|
||||
lines,
|
||||
operation.grep,
|
||||
);
|
||||
return {
|
||||
op: "logs",
|
||||
name: record.snapshot.name,
|
||||
text,
|
||||
text: output.text,
|
||||
terminalText: record.spec.pty && operation.grep === undefined ? output.terminalText : undefined,
|
||||
cursor: record.snapshot.outputBytes,
|
||||
timedOut,
|
||||
state: record.snapshot.state,
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
/** Hidden CLI selector used to re-enter the daemon broker worker. */
|
||||
export const DAEMON_BROKER_WORKER_ARG = "__omp_worker_daemon_broker";
|
||||
|
||||
/** Fixed dimensions negotiated with every supervised PTY. */
|
||||
export const DAEMON_PTY_COLUMNS = 120;
|
||||
export const DAEMON_PTY_ROWS = 40;
|
||||
|
||||
/** Environment key carrying the broker's canonical project directory. */
|
||||
export const DAEMON_PROJECT_DIR_ENV = "OMP_DAEMON_PROJECT_DIR";
|
||||
|
||||
@@ -97,6 +101,8 @@ export type DaemonRpcResult =
|
||||
op: "logs";
|
||||
name: string;
|
||||
text: string;
|
||||
/** Raw PTY byte stream used only to reconstruct the terminal screen. */
|
||||
terminalText?: string;
|
||||
cursor: number;
|
||||
timedOut: boolean;
|
||||
state: DaemonState;
|
||||
@@ -349,6 +355,8 @@ export function parseDaemonRpcResult(operation: DaemonOperation, value: unknown)
|
||||
op: "logs",
|
||||
name: stringValue(source.name, "result.name"),
|
||||
text: typeof source.text === "string" ? source.text : "",
|
||||
terminalText:
|
||||
source.terminalText === undefined ? undefined : rawString(source.terminalText, "result.terminalText"),
|
||||
cursor: numberValue(source.cursor, "result.cursor"),
|
||||
timedOut: booleanValue(source.timedOut, "result.timedOut"),
|
||||
state: daemonState(source.state),
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { logger } from "@oh-my-pi/pi-utils";
|
||||
import xterm, { type Terminal as XtermTerminal } from "@xterm/headless";
|
||||
import { readTerminalRows } from "../tools/terminal-output";
|
||||
import { DAEMON_PTY_COLUMNS, DAEMON_PTY_ROWS } from "./protocol";
|
||||
|
||||
const VIRTUAL_SCROLLBACK_ROWS = 4_096;
|
||||
|
||||
/** Controls which virtual terminal rows a launch log exposes. */
|
||||
export interface TerminalOutputOptions {
|
||||
head: boolean;
|
||||
maxRows: number;
|
||||
}
|
||||
|
||||
function writeTerminal(terminal: XtermTerminal, output: string): Promise<void> {
|
||||
const { promise, resolve } = Promise.withResolvers<void>();
|
||||
terminal.write(output, resolve);
|
||||
return promise;
|
||||
}
|
||||
|
||||
/** Replays daemon bytes with the same xterm screen renderer used by PTY mode. */
|
||||
export async function renderTerminalOutput(
|
||||
output: string,
|
||||
options: TerminalOutputOptions,
|
||||
): Promise<string[] | undefined> {
|
||||
if (!output) return [];
|
||||
const maxRows = Math.max(1, Math.floor(options.maxRows));
|
||||
const terminal = new xterm.Terminal({
|
||||
cols: DAEMON_PTY_COLUMNS,
|
||||
rows: DAEMON_PTY_ROWS,
|
||||
scrollback: Math.max(VIRTUAL_SCROLLBACK_ROWS, maxRows),
|
||||
allowProposedApi: true,
|
||||
});
|
||||
try {
|
||||
await writeTerminal(terminal, output);
|
||||
const rows = readTerminalRows(terminal, 0, terminal.buffer.active.length);
|
||||
while (rows.at(-1) === "") rows.pop();
|
||||
return options.head ? rows.slice(0, maxRows) : rows.slice(-maxRows);
|
||||
} catch (error) {
|
||||
logger.debug("Failed to render launch terminal output", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return undefined;
|
||||
} finally {
|
||||
terminal.dispose();
|
||||
}
|
||||
}
|
||||
@@ -852,6 +852,7 @@ export async function buildSessionOptions(
|
||||
cliProvider: parsed.provider,
|
||||
cliModel: parsed.model,
|
||||
modelRegistry,
|
||||
settings: activeSettings,
|
||||
preferences: modelMatchPreferences,
|
||||
});
|
||||
if (resolved.warning) {
|
||||
@@ -905,40 +906,40 @@ export async function buildSessionOptions(
|
||||
if (!options.model) options.model = scopedModels[0].model;
|
||||
}
|
||||
|
||||
if (parsed.noDownshift && (parsed.downshift || parsed.downshiftInto !== undefined)) {
|
||||
throw new Error("--no-downshift cannot be combined with --downshift or --downshift-into");
|
||||
if (parsed.noPrewalk && (parsed.prewalk || parsed.prewalkInto !== undefined)) {
|
||||
throw new Error("--no-prewalk cannot be combined with --prewalk or --prewalk-into");
|
||||
}
|
||||
const downshiftEnabled = parsed.noDownshift
|
||||
const prewalkEnabled = parsed.noPrewalk
|
||||
? false
|
||||
: parsed.downshift === true || parsed.downshiftInto !== undefined
|
||||
: parsed.prewalk === true || parsed.prewalkInto !== undefined
|
||||
? true
|
||||
: activeSettings.get("downshift.enabled");
|
||||
if (downshiftEnabled) {
|
||||
const rolePattern = expandRoleAlias(parsed.downshiftInto ?? "pi/smol", activeSettings);
|
||||
: activeSettings.get("prewalk.enabled");
|
||||
if (prewalkEnabled) {
|
||||
const rolePattern = expandRoleAlias(parsed.prewalkInto ?? "@smol", activeSettings);
|
||||
const resolved = resolveCliModel({ cliModel: rolePattern, modelRegistry, preferences: modelMatchPreferences });
|
||||
if (resolved.warning) {
|
||||
process.stderr.write(`${chalk.yellow(`Warning: ${resolved.warning}`)}\n`);
|
||||
}
|
||||
if (resolved.error || !resolved.model) {
|
||||
throw new Error(resolved.error ?? `Model "${parsed.downshiftInto ?? "pi/smol"}" not found`);
|
||||
throw new Error(resolved.error ?? `Model "${parsed.prewalkInto ?? "@smol"}" not found`);
|
||||
}
|
||||
if (!modelRegistry.hasConfiguredAuth(resolved.model)) {
|
||||
throw new Error(`No API key for ${resolved.model.provider}/${resolved.model.id}`);
|
||||
}
|
||||
options.downshift = { target: resolved.model, thinkingLevel: resolved.thinkingLevel };
|
||||
options.prewalk = { target: resolved.model, thinkingLevel: resolved.thinkingLevel };
|
||||
}
|
||||
|
||||
if (parsed.planYoloInto !== undefined && !parsed.planYolo) {
|
||||
throw new Error("--plan-yolo-into requires --plan-yolo");
|
||||
}
|
||||
if (parsed.planYolo) {
|
||||
const rolePattern = expandRoleAlias(parsed.planYoloInto ?? "pi/smol", activeSettings);
|
||||
const rolePattern = expandRoleAlias(parsed.planYoloInto ?? "@smol", activeSettings);
|
||||
const resolved = resolveCliModel({ cliModel: rolePattern, modelRegistry, preferences: modelMatchPreferences });
|
||||
if (resolved.warning) {
|
||||
process.stderr.write(`${chalk.yellow(`Warning: ${resolved.warning}`)}\n`);
|
||||
}
|
||||
if (resolved.error || !resolved.model) {
|
||||
throw new Error(resolved.error ?? `Model "${parsed.planYoloInto ?? "pi/smol"}" not found`);
|
||||
throw new Error(resolved.error ?? `Model "${parsed.planYoloInto ?? "@smol"}" not found`);
|
||||
}
|
||||
if (!modelRegistry.hasConfiguredAuth(resolved.model)) {
|
||||
throw new Error(`No API key for ${resolved.model.provider}/${resolved.model.id}`);
|
||||
@@ -1183,6 +1184,7 @@ export async function runRootCommand(
|
||||
modelPatterns,
|
||||
modelRegistry,
|
||||
modelMatchPreferences,
|
||||
settingsInstance,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -351,12 +351,19 @@ async function elicitFromAcpClient(
|
||||
finish(undefined);
|
||||
});
|
||||
const response = await promise;
|
||||
if (response?.action !== "accept" || !response.content) {
|
||||
if (!isAcceptedElicitation(response) || !response.content) {
|
||||
return undefined;
|
||||
}
|
||||
return response.content.value;
|
||||
}
|
||||
|
||||
/** Narrows a `CreateElicitationResponse` to the accepted-with-content branch; the SDK's `action: string` catch-all arm otherwise defeats literal narrowing on `action !== "accept"`. */
|
||||
function isAcceptedElicitation(
|
||||
response: CreateElicitationResponse | undefined,
|
||||
): response is Extract<CreateElicitationResponse, { action: "accept" }> {
|
||||
return response?.action === "accept";
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an {@link ExtensionUIContext} that translates skill/extension UI
|
||||
* requests into ACP elicitations against `connection` for the session
|
||||
|
||||
@@ -4,15 +4,43 @@ import type { AgentSession } from "../../../session/agent-session";
|
||||
import { getThemeByName, setThemeInstance } from "../../theme/theme";
|
||||
import { StatusLineComponent } from "./component";
|
||||
|
||||
function makeSessionWithLastMessage(lastMessage: unknown) {
|
||||
function makeSessionWithLastMessage(lastMessage: unknown, prewalkArmed: boolean = false) {
|
||||
return {
|
||||
messages: [lastMessage],
|
||||
messages: lastMessage ? [lastMessage] : [],
|
||||
model: { contextWindow: 128000 },
|
||||
contextUsageRevision: 0,
|
||||
systemPrompt: [],
|
||||
agent: { state: { tools: [] } },
|
||||
skills: [],
|
||||
getContextUsage: () => ({ tokens: 42, contextWindow: 128000 }),
|
||||
state: {
|
||||
messages: lastMessage ? [lastMessage] : [],
|
||||
model: { contextWindow: 128000 },
|
||||
},
|
||||
sessionManager: {
|
||||
getUsageStatistics: () => ({
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
orchestrationInput: 0,
|
||||
orchestrationOutput: 0,
|
||||
orchestrationCacheRead: 0,
|
||||
premiumRequests: 0,
|
||||
cost: 0,
|
||||
tokensPerSecond: null,
|
||||
}),
|
||||
getSessionName: () => "test-session",
|
||||
},
|
||||
getPrewalkState: () => (prewalkArmed ? { target: { id: "cheap-model", provider: "openai" } } : undefined),
|
||||
getAsyncJobSnapshot: () => undefined,
|
||||
isAdvisorActive: () => false,
|
||||
isFastModeActive: () => false,
|
||||
configuredThinkingLevel: () => undefined,
|
||||
modelRegistry: {
|
||||
isUsingOAuth: () => false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,4 +69,15 @@ describe("StatusLineComponent", () => {
|
||||
|
||||
expect(statusLine.getCachedContextBreakdown()).toEqual({ usedTokens: 42, contextWindow: 128000 });
|
||||
});
|
||||
|
||||
it("renders Prewalk annotation when prewalk is armed", () => {
|
||||
const statusLine = new StatusLineComponent(makeSessionWithLastMessage(null, true) as unknown as AgentSession);
|
||||
|
||||
// By default preset, 'mode' segment is included in left/right segments.
|
||||
// Let's get the border and see if Prewalk is rendered.
|
||||
const border = statusLine.getTopBorder(100);
|
||||
// SGR codes might be included, so we check if the stripped content contains "Prewalk"
|
||||
const stripped = border.content.replace(/\x1b\[[0-9;]*m/g, "");
|
||||
expect(stripped).toContain("Prewalk");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1051,6 +1051,10 @@ export class StatusLineComponent implements Component {
|
||||
compactThinkingLevel: this.#resolveSettings().compactThinkingLevel ?? false,
|
||||
planMode: this.#planModeStatus,
|
||||
loopMode: this.#loopModeStatus,
|
||||
prewalk:
|
||||
typeof this.session.getPrewalkState === "function" && this.session.getPrewalkState()
|
||||
? { enabled: true }
|
||||
: null,
|
||||
goalMode: this.#goalModeStatus,
|
||||
vibeMode: this.#vibeModeStatus,
|
||||
collab: this.#collabStatus,
|
||||
|
||||
@@ -230,6 +230,12 @@ const modeSegment: StatusLineSegment = {
|
||||
return { content: theme.fg(color, content), visible: true };
|
||||
}
|
||||
|
||||
const prewalk = ctx.prewalk;
|
||||
if (prewalk?.enabled) {
|
||||
const content = withIcon(theme.icon.prewalk, "Prewalk");
|
||||
return { content: theme.fg("accent", content), visible: true };
|
||||
}
|
||||
|
||||
const goal = ctx.goalMode;
|
||||
if (goal && (goal.enabled || goal.paused)) {
|
||||
return renderGoalMode(ctx, goal);
|
||||
|
||||
@@ -60,6 +60,9 @@ export interface SegmentContext {
|
||||
enabled: boolean;
|
||||
paused: boolean;
|
||||
} | null;
|
||||
prewalk: {
|
||||
enabled: boolean;
|
||||
} | null;
|
||||
loopMode: {
|
||||
enabled: boolean;
|
||||
} | null;
|
||||
|
||||
@@ -473,6 +473,10 @@ export class SelectorController {
|
||||
this.ctx.ui.requestRender();
|
||||
break;
|
||||
|
||||
case "tui.scrollbackRebuild":
|
||||
this.ctx.ui.setScrollbackRebuild(value as boolean);
|
||||
break;
|
||||
|
||||
case "tui.renderMermaid":
|
||||
setMarkdownMermaidRendering(value as boolean);
|
||||
this.ctx.session.refreshBaseSystemPrompt().catch(err => {
|
||||
|
||||
@@ -665,6 +665,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
setMarkdownMermaidRendering(settings.get("tui.renderMermaid"));
|
||||
this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
|
||||
this.ui.setMaxInlineImages(settings.get("tui.maxInlineImages"));
|
||||
this.ui.setScrollbackRebuild(settings.get("tui.scrollbackRebuild"));
|
||||
// OSC 66 text-sizing is Kitty-only; resolve the setting against the terminal's
|
||||
// capability (`TERMINAL.textSizing` defaults on for Kitty) so it stays off
|
||||
// unless the user opts in, and never emits raw escapes on other terminals.
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Contract: `--mode json` output stays linear in conversation size. Streaming
|
||||
* `message_update` events must not re-serialize the in-progress message, and
|
||||
* printed messages must not carry provider-opaque replay payloads (encrypted
|
||||
* reasoning history), which previously produced multi-GB transcripts.
|
||||
*/
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
||||
import type { AgentSessionEvent } from "../session/agent-session";
|
||||
import { printableEvent } from "./print-mode";
|
||||
|
||||
const assistant: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
model: "gpt-test",
|
||||
usage: {} as AssistantMessage["usage"],
|
||||
stopReason: "stop",
|
||||
timestamp: 1,
|
||||
providerPayload: { type: "openaiResponsesHistory", provider: "openai", dt: true, items: [{ big: "blob" }] },
|
||||
};
|
||||
|
||||
describe("printableEvent", () => {
|
||||
it("emits only the incremental delta for message_update", () => {
|
||||
const event: AgentSessionEvent = {
|
||||
type: "message_update",
|
||||
message: assistant,
|
||||
assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta: "hel", partial: assistant },
|
||||
};
|
||||
const printed = JSON.parse(JSON.stringify(printableEvent(event)));
|
||||
expect(printed).toEqual({
|
||||
type: "message_update",
|
||||
assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta: "hel" },
|
||||
});
|
||||
});
|
||||
|
||||
it("drops the done-variant message snapshot from message_update", () => {
|
||||
const event: AgentSessionEvent = {
|
||||
type: "message_update",
|
||||
message: assistant,
|
||||
assistantMessageEvent: { type: "done", reason: "stop", message: assistant },
|
||||
};
|
||||
const printed = JSON.parse(JSON.stringify(printableEvent(event)));
|
||||
expect(printed).toEqual({ type: "message_update", assistantMessageEvent: { type: "done", reason: "stop" } });
|
||||
});
|
||||
|
||||
it("strips providerPayload from message_end but keeps the message content", () => {
|
||||
const event: AgentSessionEvent = { type: "message_end", message: assistant };
|
||||
const printed = JSON.parse(JSON.stringify(printableEvent(event))) as {
|
||||
message: Record<string, unknown>;
|
||||
};
|
||||
expect(printed.message.providerPayload).toBeUndefined();
|
||||
expect(printed.message.content).toEqual([{ type: "text", text: "hello" }]);
|
||||
expect(printed.message.model).toBe("gpt-test");
|
||||
});
|
||||
|
||||
it("strips providerPayload from every message in agent_end", () => {
|
||||
const event: AgentSessionEvent = { type: "agent_end", messages: [assistant, assistant] };
|
||||
const printed = JSON.parse(JSON.stringify(printableEvent(event))) as {
|
||||
messages: Array<Record<string, unknown>>;
|
||||
};
|
||||
expect(printed.messages).toHaveLength(2);
|
||||
for (const message of printed.messages) expect(message.providerPayload).toBeUndefined();
|
||||
});
|
||||
|
||||
it("passes unrelated events through untouched", () => {
|
||||
const event: AgentSessionEvent = { type: "notice", level: "info", message: "hi" };
|
||||
expect(printableEvent(event)).toBe(event);
|
||||
});
|
||||
});
|
||||
@@ -5,9 +5,10 @@
|
||||
* - `omp -p "prompt"` - text output
|
||||
* - `omp --mode json "prompt"` - JSON event stream
|
||||
*/
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
||||
import { logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
||||
import type { AgentSession } from "../session/agent-session";
|
||||
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
||||
import { isSilentAbort } from "../session/messages";
|
||||
import { flushTelemetryExport } from "../telemetry-export";
|
||||
import { initializeExtensions } from "./runtime-init";
|
||||
@@ -28,6 +29,54 @@ export interface PrintModeOptions {
|
||||
printThoughts?: boolean;
|
||||
}
|
||||
|
||||
/** Drop the provider-opaque replay payload (e.g. encrypted reasoning items) before printing. */
|
||||
function stripProviderPayload<T extends AgentMessage>(message: T): T {
|
||||
if (!("providerPayload" in message) || message.providerPayload === undefined) return message;
|
||||
const { providerPayload: _providerPayload, ...rest } = message;
|
||||
return rest as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shape an event for `--mode json` output.
|
||||
*
|
||||
* Removes two classes of bloat so transcripts grow linearly with conversation
|
||||
* size instead of quadratically (a single long turn used to re-serialize its
|
||||
* whole in-progress message on every streamed delta, producing multi-GB logs):
|
||||
* - `message_update` snapshots (`message`, `assistantMessageEvent.partial`,
|
||||
* and the `done`/`error` payloads) are dropped; only the incremental delta
|
||||
* is printed. The authoritative message follows in `message_end`.
|
||||
* - `providerPayload` is transport-native replay state, opaque and useless
|
||||
* outside this process.
|
||||
*/
|
||||
export function printableEvent(event: AgentSessionEvent): unknown {
|
||||
switch (event.type) {
|
||||
case "message_update": {
|
||||
const streamEvent = event.assistantMessageEvent;
|
||||
if (streamEvent.type === "done" || streamEvent.type === "error") {
|
||||
return {
|
||||
type: "message_update",
|
||||
assistantMessageEvent: { type: streamEvent.type, reason: streamEvent.reason },
|
||||
};
|
||||
}
|
||||
const { partial: _partial, ...rest } = streamEvent;
|
||||
return { type: "message_update", assistantMessageEvent: rest };
|
||||
}
|
||||
case "message_start":
|
||||
case "message_end":
|
||||
return { ...event, message: stripProviderPayload(event.message) };
|
||||
case "turn_end":
|
||||
return {
|
||||
...event,
|
||||
message: stripProviderPayload(event.message),
|
||||
toolResults: event.toolResults.map(stripProviderPayload),
|
||||
};
|
||||
case "agent_end":
|
||||
return { ...event, messages: event.messages.map(stripProviderPayload) };
|
||||
default:
|
||||
return event;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run in print (single-shot) mode.
|
||||
* Sends prompts to the agent and outputs the result.
|
||||
@@ -58,7 +107,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
||||
session.subscribe(event => {
|
||||
// In JSON mode, output all events
|
||||
if (mode === "json") {
|
||||
process.stdout.write(`${JSON.stringify(event)}\n`);
|
||||
process.stdout.write(`${JSON.stringify(printableEvent(event))}\n`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ export type SymbolKey =
|
||||
// Icons
|
||||
| "icon.model"
|
||||
| "icon.plan"
|
||||
| "icon.prewalk"
|
||||
| "icon.goal"
|
||||
| "icon.pause"
|
||||
| "icon.loop"
|
||||
@@ -301,6 +302,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
||||
// Icons
|
||||
"icon.model": "⬢",
|
||||
"icon.plan": "🗺",
|
||||
"icon.prewalk": "🏃",
|
||||
"icon.goal": "🎯",
|
||||
"icon.pause": "⏸",
|
||||
"icon.loop": "↻",
|
||||
@@ -562,6 +564,7 @@ const NERD_SYMBOLS: SymbolMap = {
|
||||
"icon.model": "\uec19",
|
||||
// pick: | alt:
|
||||
"icon.plan": "\uf2d2",
|
||||
"icon.prewalk": "\uf29d",
|
||||
// pick: (nf-fa-bullseye) | alt: (nf-md-target) ◎ ⌖
|
||||
"icon.goal": "\uf140",
|
||||
// pick: (nf-fa-pause) | alt: ⏸ ||
|
||||
@@ -819,6 +822,7 @@ const ASCII_SYMBOLS: SymbolMap = {
|
||||
// Icons
|
||||
"icon.model": "[M]",
|
||||
"icon.plan": "plan",
|
||||
"icon.prewalk": "prewalk",
|
||||
"icon.goal": "goal",
|
||||
"icon.pause": "||",
|
||||
"icon.loop": "loop",
|
||||
@@ -1819,6 +1823,7 @@ export class Theme {
|
||||
return {
|
||||
model: this.#symbols["icon.model"],
|
||||
plan: this.#symbols["icon.plan"],
|
||||
prewalk: this.#symbols["icon.prewalk"],
|
||||
goal: this.#symbols["icon.goal"],
|
||||
pause: this.#symbols["icon.pause"],
|
||||
loop: this.#symbols["icon.loop"],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: designer
|
||||
description: UI/UX specialist for design implementation, review, visual refinement
|
||||
model: pi/designer
|
||||
model: "@designer"
|
||||
---
|
||||
|
||||
Implement and review UI designs. Edit files, create components, run commands when needed.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: librarian
|
||||
description: Researches external libraries and APIs by reading source code. Returns definitive, source-verified answers.
|
||||
tools: read, grep, glob, bash, lsp, web_search, ast_grep
|
||||
model: pi/smol
|
||||
model: "@smol"
|
||||
thinking-level: minimal
|
||||
read-summarize: false
|
||||
output:
|
||||
|
||||
@@ -3,7 +3,7 @@ name: reviewer
|
||||
description: "Code review specialist for quality/security analysis"
|
||||
tools: read, grep, glob, bash, lsp, web_search, ast_grep
|
||||
spawns: scout
|
||||
model: pi/slow
|
||||
model: "@slow"
|
||||
output:
|
||||
properties:
|
||||
overall_correctness:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: scout
|
||||
description: MUST be used for exploratory codebase research, rapid code analysis, and broad pattern searches. Fast read-only scout returning compressed context for handoff.
|
||||
tools: read, grep, glob, web_search
|
||||
model: pi/smol
|
||||
model: "@smol"
|
||||
thinking-level: medium
|
||||
read-summarize: false
|
||||
output:
|
||||
|
||||
+1
-1
@@ -8,6 +8,6 @@ First, state the plan itself, explicitly and comprehensively:
|
||||
|
||||
Be thorough and concrete — this plan is the reference for the remainder of the run. You may verify details with tools after the plan is written, never before.
|
||||
|
||||
Then, only once the plan above is complete and detailed, in the SAME reply, capture it as a SUPER-DETAILED todo list (the todo tool): one item per concrete step from the plan — each naming its exact file/symbol/command target and its verification — not a handful of vague phase headings. The todo list must be precise enough that every item can be checked off against an observable result.
|
||||
Then, only once the plan above is complete, in the SAME reply, capture it as a todo list (the todo tool): 5-9 items, one per MEANINGFUL step, each naming its concrete target and its verification. Only steps that change or verify code belong on the list — no reporting, bookkeeping, cleanup-ceremony, or release-note items. The todo list serves the task, never the reverse: when reality disagrees with an item, fix the actual problem rather than working the checklist.
|
||||
|
||||
This is a checkpoint, not a final answer: do not end your turn on the plan alone — after recording the todo list, continue the task; do not stop here.
|
||||
@@ -1,5 +1,5 @@
|
||||
<system-notice cause="fork">
|
||||
The conversation above belongs to your parent session.
|
||||
The conversation above belongs to your parent session.
|
||||
You are a fork created solely to handle the user's request below.
|
||||
|
||||
Your parent agent is still working on the original task — that responsibility is
|
||||
|
||||
@@ -109,7 +109,7 @@ import {
|
||||
obfuscateProviderContext,
|
||||
SecretObfuscator,
|
||||
} from "./secrets";
|
||||
import { AgentSession, type Downshift, type PlanYolo } from "./session/agent-session";
|
||||
import { AgentSession, type PlanYolo, type Prewalk } from "./session/agent-session";
|
||||
import { discoverAuthStorage as discoverAuthStorageFromConfig } from "./session/auth-broker-config";
|
||||
import type { AuthStorage } from "./session/auth-storage";
|
||||
import {
|
||||
@@ -407,8 +407,8 @@ export interface CreateAgentSessionOptions {
|
||||
thinkingLevel?: ConfiguredThinkingLevel;
|
||||
/** Models available for cycling (Ctrl+P in interactive mode) */
|
||||
scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
||||
/** Downshift from the starting model to a fast/cheap target at the first edit/write once the todo list exists. */
|
||||
downshift?: Downshift;
|
||||
/** Prewalk from the starting model to a fast/cheap target at the first edit/write once the todo list exists. */
|
||||
prewalk?: Prewalk;
|
||||
/** Force read-only plan mode at start, auto-approve on the model's first resolve call, then switch to execute. */
|
||||
planYolo?: PlanYolo;
|
||||
|
||||
@@ -1987,7 +1987,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
}
|
||||
}
|
||||
// Resolve deferred --model/subagent patterns now that extension models are
|
||||
// registered. Expand role aliases (`pi/smol`) and comma chains to concrete
|
||||
// registered. Expand role aliases (`@smol`) and comma chains to concrete
|
||||
// selectors first so deferred resolution accepts everything the immediate
|
||||
// path (resolveModelOverride → resolveModelRoleValue) accepts.
|
||||
if (!model && deferredModelPatterns.length > 0) {
|
||||
@@ -2867,7 +2867,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
agent,
|
||||
pruneToolDescriptions: inlineToolDescriptors,
|
||||
thinkingLevel: autoThinking ? AUTO_THINKING : effectiveThinkingLevel,
|
||||
downshift: options.downshift,
|
||||
prewalk: options.prewalk,
|
||||
planYolo: options.planYolo,
|
||||
serviceTierByFamily: initialServiceTierByFamily,
|
||||
sessionManager,
|
||||
|
||||
@@ -260,9 +260,6 @@ import goalModeContextPrompt from "../prompts/goals/goal-mode-context.md" with {
|
||||
import goalTodoContextPrompt from "../prompts/goals/goal-todo-context.md" with { type: "text" };
|
||||
import parentIrcSteerTemplate from "../prompts/steering/parent-irc.md" with { type: "text" };
|
||||
import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
|
||||
import downshiftChecklistPrompt from "../prompts/system/downshift-checklist.md" with { type: "text" };
|
||||
import downshiftContinuePrompt from "../prompts/system/downshift-continue.md" with { type: "text" };
|
||||
import downshiftPlanPrompt from "../prompts/system/downshift-plan.md" with { type: "text" };
|
||||
import eagerTaskPrompt from "../prompts/system/eager-task.md" with { type: "text" };
|
||||
import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
|
||||
import emptyStopRetryTemplate from "../prompts/system/empty-stop-retry.md" with { type: "text" };
|
||||
@@ -277,6 +274,9 @@ import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool
|
||||
type: "text",
|
||||
};
|
||||
import planYoloHandoffPrompt from "../prompts/system/plan-yolo-handoff.md" with { type: "text" };
|
||||
import prewalkChecklistPrompt from "../prompts/system/prewalk-checklist.md" with { type: "text" };
|
||||
import prewalkContinuePrompt from "../prompts/system/prewalk-continue.md" with { type: "text" };
|
||||
import prewalkPlanPrompt from "../prompts/system/prewalk-plan.md" with { type: "text" };
|
||||
import rewindReportTemplate from "../prompts/system/rewind-report.md" with { type: "text" };
|
||||
import sideChannelNoToolsReminder from "../prompts/system/side-channel-no-tools.md" with { type: "text" };
|
||||
import thinkingLoopRedirectTemplate from "../prompts/system/thinking-loop-redirect.md" with { type: "text" };
|
||||
@@ -420,29 +420,29 @@ const MID_RUN_TODO_NUDGE_MUTATING_TOOLS: Record<string, true> = {
|
||||
/** `customType` for the hidden mid-run todo nudge; `display: false`, so it reaches
|
||||
* the model but never renders in the TUI or transcript. */
|
||||
const MID_RUN_TODO_NUDGE_MESSAGE_TYPE = "mid-run-todo-nudge";
|
||||
/** Hidden plan nudge injected by downshift; scrubbed from the LLM context
|
||||
/** Hidden plan nudge injected by prewalk; scrubbed from the LLM context
|
||||
* when the switch happens. */
|
||||
const DOWNSHIFT_PLAN_MESSAGE_TYPE = "downshift-plan";
|
||||
const PREWALK_PLAN_MESSAGE_TYPE = "prewalk-plan";
|
||||
/** Hidden safety-net nudge forcing one more turn after a text-only reply to
|
||||
* the plan nudge, which would otherwise end the run with no code written. */
|
||||
const DOWNSHIFT_CONTINUE_MESSAGE_TYPE = "downshift-continue";
|
||||
const PREWALK_CONTINUE_MESSAGE_TYPE = "prewalk-continue";
|
||||
/** Hidden "verify before finishing" checklist steered into the run at the
|
||||
* switch, aimed at the fast model's specific failure patterns: partial
|
||||
* multi-site fixes, unnecessarily broad rewrites, and reported-test-only
|
||||
* verification. */
|
||||
const DOWNSHIFT_CHECKLIST_MESSAGE_TYPE = "downshift-checklist";
|
||||
const PREWALK_CHECKLIST_MESSAGE_TYPE = "prewalk-checklist";
|
||||
/** Tools whose first successful call triggers the switch — once the todo
|
||||
* gate is open (see {@link AgentSession.#downshiftTodoSeen}). Bash is
|
||||
* gate is open (see {@link AgentSession.#prewalkTodoSeen}). Bash is
|
||||
* deliberately excluded: it doubles as exploration (ls/cat) and fired
|
||||
* turn-1 switches in practice. `todo` is deliberately NOT a trigger: firing
|
||||
* at the todo init handed the fast model 100% of the implementation with
|
||||
* zero started work and measurably regressed pass rates. */
|
||||
const DOWNSHIFT_ACTION_TOOLS: Record<string, true> = {
|
||||
const PREWALK_ACTION_TOOLS: Record<string, true> = {
|
||||
edit: true,
|
||||
write: true,
|
||||
};
|
||||
/** `customType` for the hidden hand-off message steered to the target model
|
||||
* once PlanYolo auto-approves the plan. Unlike downshift's plan nudge this
|
||||
* once PlanYolo auto-approves the plan. Unlike prewalk's plan nudge this
|
||||
* is never scrubbed — it IS the instruction the target model acts on. */
|
||||
const PLAN_YOLO_HANDOFF_MESSAGE_TYPE = "plan-yolo-handoff";
|
||||
/** Abort reason for the Gemini reasoning-header runaway interrupt. Surfaced on the
|
||||
@@ -713,7 +713,7 @@ export interface AsyncJobSnapshot {
|
||||
|
||||
export type { ShakeMode, ShakeResult };
|
||||
/**
|
||||
* Downshift: switches an active session one-way from its starting model to
|
||||
* Prewalk: switches an active session one-way from its starting model to
|
||||
* a fast/cheap `target` at the first completed turn that runs an edit/write
|
||||
* tool once the todo list exists. A hidden plan nudge asks the starting
|
||||
* model to write a plan, initialize its todo list from it, and start; the
|
||||
@@ -723,7 +723,7 @@ export type { ShakeMode, ShakeResult };
|
||||
* finishing. Both are always on — this is the one mechanism that won out
|
||||
* over turn-count and ungated variants in testing.
|
||||
*/
|
||||
export interface Downshift {
|
||||
export interface Prewalk {
|
||||
target: Model;
|
||||
thinkingLevel?: ConfiguredThinkingLevel;
|
||||
}
|
||||
@@ -755,8 +755,8 @@ export interface AgentSessionConfig {
|
||||
scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
||||
/** Initial session thinking selector. */
|
||||
thinkingLevel?: ConfiguredThinkingLevel;
|
||||
/** Downshift from the starting model to a fast/cheap target at the first edit/write once the todo list exists. */
|
||||
downshift?: Downshift;
|
||||
/** Prewalk from the starting model to a fast/cheap target at the first edit/write once the todo list exists. */
|
||||
prewalk?: Prewalk;
|
||||
/** Force read-only plan mode at start, auto-approve on the model's first
|
||||
* `resolve` call, then switch to the target to implement. */
|
||||
planYolo?: PlanYolo;
|
||||
@@ -1678,13 +1678,13 @@ export class AgentSession {
|
||||
#autoThinking: boolean = false;
|
||||
/** The level `auto` last resolved to (for UI); undefined until a turn is classified. */
|
||||
#autoResolvedLevel: Effort | undefined;
|
||||
#downshift: Downshift | undefined;
|
||||
#prewalk: Prewalk | undefined;
|
||||
/** True once the plan nudge has been queued; scrubbed from context at the switch. */
|
||||
#downshiftPlanInjected = false;
|
||||
/** True once any successful `todo` call landed — opens the downshift
|
||||
#prewalkPlanInjected = false;
|
||||
/** True once any successful `todo` call landed — opens the prewalk
|
||||
* trigger gate: the switch fires at the first edit/write AFTER the todo
|
||||
* list exists (sessions without a todo tool skip the gate). */
|
||||
#downshiftTodoSeen = false;
|
||||
#prewalkTodoSeen = false;
|
||||
#planYolo: PlanYolo | undefined;
|
||||
#planYoloPreviousTools: string[] | undefined;
|
||||
#planYoloArmed = false;
|
||||
@@ -2178,10 +2178,10 @@ export class AgentSession {
|
||||
this.#emit(pending);
|
||||
}
|
||||
|
||||
/** Advance the one-way downshift switch at a completed assistant-turn boundary. */
|
||||
async #advanceDownshift(liveMessages: AgentMessage[], context: AgentTurnEndContext | undefined): Promise<void> {
|
||||
const downshift = this.#downshift;
|
||||
if (!downshift || context?.message.role !== "assistant") return;
|
||||
/** Advance the one-way prewalk switch at a completed assistant-turn boundary. */
|
||||
async #advancePrewalk(liveMessages: AgentMessage[], context: AgentTurnEndContext | undefined): Promise<void> {
|
||||
const prewalk = this.#prewalk;
|
||||
if (!prewalk || context?.message.role !== "assistant") return;
|
||||
|
||||
// Structural safety net: every branch below assumes the agent loop will
|
||||
// run another turn. It won't if THIS turn had no tool calls — the loop
|
||||
@@ -2191,11 +2191,11 @@ export class AgentSession {
|
||||
// silently killing production SWE-bench runs before any code was ever
|
||||
// written. Force one more turn only in that specific, self-created
|
||||
// hazard window.
|
||||
if (this.#downshiftPlanInjected && context.toolResults.length === 0) {
|
||||
if (this.#prewalkPlanInjected && context.toolResults.length === 0) {
|
||||
this.agent.steer({
|
||||
role: "custom",
|
||||
customType: DOWNSHIFT_CONTINUE_MESSAGE_TYPE,
|
||||
content: downshiftContinuePrompt,
|
||||
customType: PREWALK_CONTINUE_MESSAGE_TYPE,
|
||||
content: prewalkContinuePrompt,
|
||||
attribution: "agent",
|
||||
display: false,
|
||||
timestamp: Date.now(),
|
||||
@@ -2209,24 +2209,24 @@ export class AgentSession {
|
||||
// the fast model the whole implementation cold. Sessions without a todo
|
||||
// tool skip the gate.
|
||||
if (context.toolResults.some(result => result.toolName === "todo")) {
|
||||
this.#downshiftTodoSeen = true;
|
||||
this.#prewalkTodoSeen = true;
|
||||
}
|
||||
const todoGateOpen = this.#downshiftTodoSeen || !this.#toolRegistry.has("todo");
|
||||
const todoGateOpen = this.#prewalkTodoSeen || !this.#toolRegistry.has("todo");
|
||||
const action = todoGateOpen
|
||||
? context.toolResults.find(result => DOWNSHIFT_ACTION_TOOLS[result.toolName])
|
||||
? context.toolResults.find(result => PREWALK_ACTION_TOOLS[result.toolName])
|
||||
: undefined;
|
||||
if (!action) {
|
||||
if (!this.#downshiftPlanInjected) {
|
||||
this.#downshiftPlanInjected = true;
|
||||
if (!this.#prewalkPlanInjected) {
|
||||
this.#prewalkPlanInjected = true;
|
||||
this.agent.steer({
|
||||
role: "custom",
|
||||
customType: DOWNSHIFT_PLAN_MESSAGE_TYPE,
|
||||
content: downshiftPlanPrompt,
|
||||
customType: PREWALK_PLAN_MESSAGE_TYPE,
|
||||
content: prewalkPlanPrompt,
|
||||
display: false,
|
||||
attribution: "agent",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
this.emitNotice("info", "Downshift: injected deep-plan nudge.", "downshift");
|
||||
this.emitNotice("info", "Prewalk: injected deep-plan nudge.", "prewalk");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -2236,24 +2236,24 @@ export class AgentSession {
|
||||
await this.#waitForSessionMessagePersistence(toolResult);
|
||||
}
|
||||
|
||||
this.#scrubDownshiftPlanNudge(liveMessages);
|
||||
const target = downshift.target;
|
||||
this.#scrubPrewalkPlanNudge(liveMessages);
|
||||
const target = prewalk.target;
|
||||
if (this.model && modelsAreEqual(this.model, target)) {
|
||||
this.#downshift = undefined;
|
||||
this.#prewalk = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.setModelTemporary(target, downshift.thinkingLevel, { ephemeral: true });
|
||||
this.#downshift = undefined;
|
||||
await this.setModelTemporary(target, prewalk.thinkingLevel, { ephemeral: true });
|
||||
this.#prewalk = undefined;
|
||||
this.emitNotice(
|
||||
"info",
|
||||
`Downshift: switched to ${target.provider}/${target.id} after first ${action.toolName} call.`,
|
||||
"downshift",
|
||||
`Prewalk: switched to ${target.provider}/${target.id} after first ${action.toolName} call.`,
|
||||
"prewalk",
|
||||
);
|
||||
this.agent.steer({
|
||||
role: "custom",
|
||||
customType: DOWNSHIFT_CHECKLIST_MESSAGE_TYPE,
|
||||
content: downshiftChecklistPrompt,
|
||||
customType: PREWALK_CHECKLIST_MESSAGE_TYPE,
|
||||
content: prewalkChecklistPrompt,
|
||||
attribution: "agent",
|
||||
display: false,
|
||||
timestamp: Date.now(),
|
||||
@@ -2261,35 +2261,35 @@ export class AgentSession {
|
||||
}
|
||||
|
||||
/**
|
||||
* Arm downshift outside the normal startup path (the `/downshift` slash
|
||||
* Arm prewalk outside the normal startup path (the `/prewalk` slash
|
||||
* command): sets the target and immediately steers the plan nudge rather
|
||||
* than waiting for the next turn boundary, since an explicit manual
|
||||
* invocation means "start this now." A no-op with a notice if a downshift
|
||||
* invocation means "start this now." A no-op with a notice if a prewalk
|
||||
* is already armed and waiting.
|
||||
*/
|
||||
armDownshift(target: Model, thinkingLevel?: ConfiguredThinkingLevel): void {
|
||||
if (this.#downshift) {
|
||||
armPrewalk(target: Model, thinkingLevel?: ConfiguredThinkingLevel): void {
|
||||
if (this.#prewalk) {
|
||||
this.emitNotice(
|
||||
"info",
|
||||
`Downshift: already armed for ${this.#downshift.target.provider}/${this.#downshift.target.id}, waiting for the first edit/write.`,
|
||||
"downshift",
|
||||
`Prewalk: already armed for ${this.#prewalk.target.provider}/${this.#prewalk.target.id}, waiting for the first edit/write.`,
|
||||
"prewalk",
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.#downshift = { target, thinkingLevel };
|
||||
this.#downshiftPlanInjected = true;
|
||||
this.#prewalk = { target, thinkingLevel };
|
||||
this.#prewalkPlanInjected = true;
|
||||
this.agent.steer({
|
||||
role: "custom",
|
||||
customType: DOWNSHIFT_PLAN_MESSAGE_TYPE,
|
||||
content: downshiftPlanPrompt,
|
||||
customType: PREWALK_PLAN_MESSAGE_TYPE,
|
||||
content: prewalkPlanPrompt,
|
||||
display: false,
|
||||
attribution: "agent",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
this.emitNotice(
|
||||
"info",
|
||||
`Downshift: armed for ${target.provider}/${target.id} — will switch at the first edit/write once the todo list exists.`,
|
||||
"downshift",
|
||||
`Prewalk: armed for ${target.provider}/${target.id} — will switch at the first edit/write once the todo list exists.`,
|
||||
"prewalk",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2299,12 +2299,12 @@ export class AgentSession {
|
||||
* Splices the loop's live context array in place (the run streams from
|
||||
* it) and mirrors the removal into agent state. The persisted transcript
|
||||
* keeps the message for audit; a session reload re-materializes it,
|
||||
* which is acceptable for downshift's single-run lifecycle.
|
||||
* which is acceptable for prewalk's single-run lifecycle.
|
||||
*/
|
||||
#scrubDownshiftPlanNudge(liveMessages: AgentMessage[]): void {
|
||||
if (!this.#downshiftPlanInjected) return;
|
||||
#scrubPrewalkPlanNudge(liveMessages: AgentMessage[]): void {
|
||||
if (!this.#prewalkPlanInjected) return;
|
||||
const isPlanNudge = (m: AgentMessage): boolean =>
|
||||
m.role === "custom" && m.customType === DOWNSHIFT_PLAN_MESSAGE_TYPE;
|
||||
m.role === "custom" && m.customType === PREWALK_PLAN_MESSAGE_TYPE;
|
||||
for (let i = liveMessages.length - 1; i >= 0; i--) {
|
||||
if (isPlanNudge(liveMessages[i])) liveMessages.splice(i, 1);
|
||||
}
|
||||
@@ -2445,8 +2445,8 @@ export class AgentSession {
|
||||
} else {
|
||||
this.#thinkingLevel = config.thinkingLevel;
|
||||
}
|
||||
if (config.downshift) {
|
||||
this.#downshift = config.downshift;
|
||||
if (config.prewalk) {
|
||||
this.#prewalk = config.prewalk;
|
||||
}
|
||||
if (config.planYolo) {
|
||||
this.#planYolo = config.planYolo;
|
||||
@@ -2525,7 +2525,7 @@ export class AgentSession {
|
||||
});
|
||||
if (detection) this.#maybeInjectToolCallLoopRedirect(messages, detection);
|
||||
}
|
||||
await this.#advanceDownshift(messages, context);
|
||||
await this.#advancePrewalk(messages, context);
|
||||
this.#advisorPrimaryTurnsCompleted++;
|
||||
if (this.#advisors.length > 0) {
|
||||
for (const a of this.#advisors) {
|
||||
@@ -7509,6 +7509,11 @@ export class AgentSession {
|
||||
return this.#planModeState;
|
||||
}
|
||||
|
||||
/** Prewalk state, if armed and active */
|
||||
getPrewalkState(): Prewalk | undefined {
|
||||
return this.#prewalk;
|
||||
}
|
||||
|
||||
setPlanModeState(state: PlanModeState | undefined): void {
|
||||
this.#planModeState = state;
|
||||
if (state?.enabled) {
|
||||
@@ -9685,7 +9690,7 @@ export class AgentSession {
|
||||
const all = this.#modelRegistry.getAvailable();
|
||||
const patterns = this.settings.get("enabledModels");
|
||||
if (!patterns || patterns.length === 0) return all;
|
||||
return filterAvailableModelsByEnabledPatterns(all, patterns);
|
||||
return filterAvailableModelsByEnabledPatterns(all, patterns, this.settings);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
@@ -462,11 +462,11 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "downshift",
|
||||
description: "Switch to a fast/cheap model at the next action (works even without --downshift)",
|
||||
acpDescription: "Downshift at the next action",
|
||||
name: "prewalk",
|
||||
description: "Switch to a fast/cheap model at the next action (works even without --prewalk)",
|
||||
acpDescription: "Prewalk at the next action",
|
||||
handle: async (_command, runtime) => {
|
||||
const rolePattern = expandRoleAlias("pi/smol", runtime.settings);
|
||||
const rolePattern = expandRoleAlias("@smol", runtime.settings);
|
||||
const resolved = resolveCliModel({
|
||||
cliModel: rolePattern,
|
||||
modelRegistry: runtime.session.modelRegistry,
|
||||
@@ -478,9 +478,9 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
||||
if (!runtime.session.modelRegistry.hasConfiguredAuth(resolved.model)) {
|
||||
return usage(`No API key for ${resolved.model.provider}/${resolved.model.id}`, runtime);
|
||||
}
|
||||
runtime.session.armDownshift(resolved.model, resolved.thinkingLevel);
|
||||
runtime.session.armPrewalk(resolved.model, resolved.thinkingLevel);
|
||||
await runtime.output(
|
||||
`Downshift on: switching to ${resolved.model.provider}/${resolved.model.id} at the next edit/write (todo-gated).`,
|
||||
`Prewalk on: switching to ${resolved.model.provider}/${resolved.model.id} at the next edit/write (todo-gated).`,
|
||||
);
|
||||
return commandConsumed();
|
||||
},
|
||||
|
||||
@@ -50,7 +50,7 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
|
||||
name: "task",
|
||||
description: "General-purpose subagent with full capabilities for delegated multi-step tasks",
|
||||
spawns: "*",
|
||||
model: "pi/task",
|
||||
model: "@task",
|
||||
thinkingLevel: AUTO_THINKING,
|
||||
},
|
||||
template: taskMd,
|
||||
@@ -60,7 +60,7 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
|
||||
frontmatter: {
|
||||
name: "sonic",
|
||||
description: "Low-reasoning agent for strictly mechanical updates or data collection only",
|
||||
model: "pi/smol",
|
||||
model: "@smol",
|
||||
thinkingLevel: Effort.Medium,
|
||||
},
|
||||
template: taskMd,
|
||||
|
||||
@@ -62,18 +62,25 @@ const THINKING_LEVEL_BY_SELECTOR: Readonly<Record<string, ThinkingLevel>> = {
|
||||
};
|
||||
|
||||
function getOwnSelector<T>(selectors: Readonly<Record<string, T>>, value: string | null | undefined): T | undefined {
|
||||
return value === undefined || value === null || !Object.hasOwn(selectors, value) ? undefined : selectors[value];
|
||||
if (value === undefined || value === null) return undefined;
|
||||
if (Object.hasOwn(selectors, value)) return selectors[value];
|
||||
// Accept unambiguous abbreviations (`xhi` → xhigh, `med` → medium) so every
|
||||
// selector surface (`--thinking`, `:suffix`, role values) parses alike.
|
||||
// Two-character minimum keeps single letters (`m`) from guessing.
|
||||
if (value.length < 2) return undefined;
|
||||
const matches = Object.keys(selectors).filter(selector => selector.startsWith(value));
|
||||
return matches.length === 1 ? selectors[matches[0]] : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a provider-facing effort value.
|
||||
* Parses a provider-facing effort value. Accepts unambiguous abbreviations.
|
||||
*/
|
||||
export function parseEffort(value: string | null | undefined): Effort | undefined {
|
||||
return getOwnSelector(EFFORT_BY_SELECTOR, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an agent-local thinking selector.
|
||||
* Parses an agent-local thinking selector. Accepts unambiguous abbreviations.
|
||||
*/
|
||||
export function parseThinkingLevel(value: string | null | undefined): ThinkingLevel | undefined {
|
||||
return getOwnSelector(THINKING_LEVEL_BY_SELECTOR, value);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Default session-title model: the online pi/smol path (no local download / on-device inference). */
|
||||
/** Default session-title model: the online @smol path (no local download / on-device inference). */
|
||||
export const ONLINE_TINY_TITLE_MODEL_KEY = "online";
|
||||
/** Local model the `tiny-models` CLI downloads when none is named. Not the session-title default — that is {@link ONLINE_TINY_TITLE_MODEL_KEY}. */
|
||||
export const DEFAULT_TINY_TITLE_LOCAL_MODEL_KEY = "lfm2-700m";
|
||||
@@ -87,9 +87,9 @@ void TINY_TITLE_MODEL_VALUES_MATCH_REGISTRY;
|
||||
export const TINY_TITLE_MODEL_OPTIONS = [
|
||||
{
|
||||
value: ONLINE_TINY_TITLE_MODEL_KEY,
|
||||
label: "Online (TINY role, else pi/smol)",
|
||||
label: "Online (TINY role, else @smol)",
|
||||
description:
|
||||
"Online title generation: the TINY model role (set one in /models) when assigned, otherwise the online fallback (commit role, then pi/smol). No local download or on-device inference.",
|
||||
"Online title generation: the TINY model role (set one in /models) when assigned, otherwise the online fallback (commit role, then @smol). No local download or on-device inference.",
|
||||
},
|
||||
...TINY_TITLE_LOCAL_MODELS.map(model => ({
|
||||
value: model.key,
|
||||
@@ -194,9 +194,9 @@ void TINY_MEMORY_MODEL_VALUES_MATCH_REGISTRY;
|
||||
export const TINY_MEMORY_MODEL_OPTIONS = [
|
||||
{
|
||||
value: ONLINE_MEMORY_MODEL_KEY,
|
||||
label: "Online (TINY role, else smol)",
|
||||
label: "Online (TINY role, else @smol)",
|
||||
description:
|
||||
"Use the online model: the TINY role from /models when set, otherwise pi/smol. No local model download or on-device inference.",
|
||||
"Use the online model: the TINY role from /models when set, otherwise @smol. No local model download or on-device inference.",
|
||||
},
|
||||
...TINY_MEMORY_LOCAL_MODELS.map(model => ({
|
||||
value: model.key,
|
||||
@@ -256,9 +256,9 @@ export type AutoThinkingModelKey = TinyMemoryModelKey;
|
||||
export const AUTO_THINKING_MODEL_OPTIONS = [
|
||||
{
|
||||
value: ONLINE_AUTO_THINKING_MODEL_KEY,
|
||||
label: "Online (TINY role, else smol)",
|
||||
label: "Online (TINY role, else @smol)",
|
||||
description:
|
||||
"Classify prompt difficulty online with the TINY role model (set one in /models) or pi/smol; no local download or on-device inference.",
|
||||
"Classify prompt difficulty online with the TINY role model (set one in /models) or @smol; no local download or on-device inference.",
|
||||
},
|
||||
...TINY_MEMORY_LOCAL_MODELS.map(model => ({
|
||||
value: model.key,
|
||||
|
||||
@@ -19,6 +19,7 @@ import { OutputSink, type OutputSummary } from "../session/streaming-output";
|
||||
import { sanitizeWithOptionalSixelPassthrough } from "../utils/sixel";
|
||||
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "./output-meta";
|
||||
import { formatStatusIcon, replaceTabs } from "./render-utils";
|
||||
import { readTerminalRows, styleTerminalRow } from "./terminal-output";
|
||||
|
||||
export interface BashInteractiveResult extends OutputSummary {
|
||||
exitCode: number | undefined;
|
||||
@@ -225,14 +226,10 @@ class BashInteractiveOverlayComponent implements Component {
|
||||
|
||||
#readViewport(innerWidth: number, maxContentRows: number): string[] {
|
||||
this.#terminal.resize(innerWidth, maxContentRows);
|
||||
const buffer = this.#terminal.buffer.active;
|
||||
const viewportY = buffer.viewportY;
|
||||
const visibleLines: string[] = [];
|
||||
for (let i = 0; i < maxContentRows; i++) {
|
||||
const line = buffer.getLine(viewportY + i)?.translateToString(true) ?? "";
|
||||
visibleLines.push(truncateToWidth(replaceTabs(sanitizeText(line)), innerWidth));
|
||||
}
|
||||
return visibleLines;
|
||||
const viewportY = this.#terminal.buffer.active.viewportY;
|
||||
return readTerminalRows(this.#terminal, viewportY, maxContentRows).map(line =>
|
||||
truncateToWidth(styleTerminalRow(line, this.uiTheme.getFgAnsi("toolOutput")), innerWidth),
|
||||
);
|
||||
}
|
||||
render(width: number): readonly string[] {
|
||||
const safeWidth = Math.max(20, width);
|
||||
|
||||
@@ -36,12 +36,18 @@ import {
|
||||
stripRawOutputArtifactNotice,
|
||||
} from "./output-meta";
|
||||
import { resolveToCwd } from "./path-utils";
|
||||
import { capPreviewLines, formatToolWorkingDirectory, previewWindowRows, replaceTabs } from "./render-utils";
|
||||
import {
|
||||
capPreviewLines,
|
||||
DEFAULT_TERMINAL_PREVIEW_LINES,
|
||||
formatToolWorkingDirectory,
|
||||
previewWindowRows,
|
||||
replaceTabs,
|
||||
} from "./render-utils";
|
||||
import { ToolAbortError, ToolError } from "./tool-errors";
|
||||
import { toolResult } from "./tool-result";
|
||||
import { clampTimeout, TOOL_TIMEOUTS } from "./tool-timeouts";
|
||||
|
||||
export const BASH_DEFAULT_PREVIEW_LINES = 10;
|
||||
export const BASH_DEFAULT_PREVIEW_LINES = DEFAULT_TERMINAL_PREVIEW_LINES;
|
||||
|
||||
const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
||||
const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
|
||||
|
||||
@@ -121,12 +121,16 @@ async function loadBrowsers(): Promise<typeof BrowsersNs> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily download Chromium on first browser launch via @puppeteer/browsers.
|
||||
* Skipped when a system Chromium (NixOS) or PUPPETEER_EXECUTABLE_PATH is set.
|
||||
* The browser is cached under ~/.omp/puppeteer (getPuppeteerDir).
|
||||
* Resolve the Chromium executable puppeteer will launch, lazily downloading it
|
||||
* on first use via @puppeteer/browsers. Skipped when a system Chromium (NixOS)
|
||||
* or PUPPETEER_EXECUTABLE_PATH is set. The browser is cached under
|
||||
* ~/.omp/puppeteer (getPuppeteerDir). Returns undefined when platform
|
||||
* detection fails (puppeteer default resolution takes over). Exported so
|
||||
* real-browser tests can probe launchability and skip on hosts missing
|
||||
* Chrome's system libraries.
|
||||
*/
|
||||
let chromiumExecutablePromise: Promise<string | undefined> | undefined;
|
||||
async function ensureChromiumExecutable(): Promise<string | undefined> {
|
||||
export async function ensureChromiumExecutable(): Promise<string | undefined> {
|
||||
const sysChrome = resolveSystemChromium();
|
||||
if (sysChrome) return sysChrome;
|
||||
const envPath = process.env.PUPPETEER_EXECUTABLE_PATH;
|
||||
|
||||
@@ -157,8 +157,8 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
|
||||
|
||||
const activeModelPattern = this.session.getActiveModelString?.() ?? this.session.getModelString?.();
|
||||
const model =
|
||||
resolvePattern("pi/vision") ??
|
||||
resolvePattern("pi/default") ??
|
||||
resolvePattern("@vision") ??
|
||||
resolvePattern("@default") ??
|
||||
resolvePattern(activeModelPattern) ??
|
||||
availableModels[0];
|
||||
if (!model) {
|
||||
|
||||
@@ -8,19 +8,21 @@ import type {
|
||||
import type { ToolExample } from "@oh-my-pi/pi-ai";
|
||||
import type { Component } from "@oh-my-pi/pi-tui";
|
||||
import { Text } from "@oh-my-pi/pi-tui";
|
||||
import { prompt } from "@oh-my-pi/pi-utils";
|
||||
import { prompt, sanitizeText } from "@oh-my-pi/pi-utils";
|
||||
import { type } from "arktype";
|
||||
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
||||
import { daemonClientForProject } from "../launch/client";
|
||||
import type { DaemonOperation, DaemonRpcResult, DaemonSnapshot, DaemonSpec, DaemonState } from "../launch/protocol";
|
||||
import { renderTerminalOutput } from "../launch/terminal-output";
|
||||
import type { Theme, ThemeColor } from "../modes/theme/theme";
|
||||
import launchDescription from "../prompts/tools/launch.md" with { type: "text" };
|
||||
import { renderStatusLine } from "../tui";
|
||||
import { framedBlock, outputBlockContentWidth, renderStatusLine } from "../tui";
|
||||
import type { ToolSession } from ".";
|
||||
import { resolveToCwd } from "./path-utils";
|
||||
import {
|
||||
capPreviewLines,
|
||||
createCachedComponent,
|
||||
DEFAULT_TERMINAL_PREVIEW_LINES,
|
||||
formatDuration,
|
||||
formatExpandHint,
|
||||
formatMoreItems,
|
||||
@@ -32,6 +34,7 @@ import {
|
||||
TRUNCATE_LENGTHS,
|
||||
truncateToWidth,
|
||||
} from "./render-utils";
|
||||
import { styleTerminalRow } from "./terminal-output";
|
||||
import { ToolError } from "./tool-errors";
|
||||
|
||||
const launchSchema = type({
|
||||
@@ -92,6 +95,8 @@ export interface LaunchToolDetails {
|
||||
timedOut?: boolean;
|
||||
/** logs: daemon lifecycle state at read time. */
|
||||
state?: DaemonState;
|
||||
/** logs: virtual terminal rows for display; model-facing content remains sanitized text. */
|
||||
terminalRows?: string[];
|
||||
/** wait: output line that satisfied the pattern. */
|
||||
matched?: string;
|
||||
/** describe: immutable launch spec backing the command/cwd detail lines. */
|
||||
@@ -243,8 +248,10 @@ function toolContent(result: DaemonRpcResult, params: LaunchParams): string {
|
||||
return result.daemons.length
|
||||
? result.daemons.map(daemon => `- ${daemonLabel(daemon)}`).join("\n")
|
||||
: "No daemons.";
|
||||
case "logs":
|
||||
return `${result.text}${result.text && !result.text.endsWith("\n") ? "\n" : ""}[${result.name}: ${result.state}; cursor=${result.cursor}${result.timedOut ? "; follow timed out" : ""}]`;
|
||||
case "logs": {
|
||||
const text = sanitizeText(result.text);
|
||||
return `${text}${text && !text.endsWith("\n") ? "\n" : ""}[${result.name}: ${result.state}; cursor=${result.cursor}${result.timedOut ? "; follow timed out" : ""}]`;
|
||||
}
|
||||
case "wait": {
|
||||
const lines = [daemonLabel(result.daemon)];
|
||||
if (result.matched) lines.push(`Matched: ${result.matched}`);
|
||||
@@ -270,14 +277,28 @@ function toolContent(result: DaemonRpcResult, params: LaunchParams): string {
|
||||
}
|
||||
}
|
||||
|
||||
function toolDetails(result: DaemonRpcResult): LaunchToolDetails {
|
||||
async function toolDetails(result: DaemonRpcResult, params: LaunchParams): Promise<LaunchToolDetails> {
|
||||
switch (result.op) {
|
||||
case "start":
|
||||
return { op: "start", daemon: result.daemon, timedOut: result.readyTimedOut };
|
||||
case "list":
|
||||
return { op: "list", daemons: result.daemons };
|
||||
case "logs":
|
||||
return { op: "logs", cursor: result.cursor, timedOut: result.timedOut, state: result.state };
|
||||
case "logs": {
|
||||
const terminalRows =
|
||||
result.terminalText === undefined
|
||||
? undefined
|
||||
: await renderTerminalOutput(result.terminalText, {
|
||||
head: params.head ?? false,
|
||||
maxRows: Math.min(1_000, Math.floor(params.lines ?? 100)),
|
||||
});
|
||||
return {
|
||||
op: "logs",
|
||||
cursor: result.cursor,
|
||||
timedOut: result.timedOut,
|
||||
state: result.state,
|
||||
terminalRows,
|
||||
};
|
||||
}
|
||||
case "wait":
|
||||
return { op: "wait", daemon: result.daemon, timedOut: result.timedOut, matched: result.matched };
|
||||
case "send":
|
||||
@@ -372,7 +393,7 @@ export class LaunchTool implements AgentTool<typeof launchSchema, LaunchToolDeta
|
||||
const result = await client.request(operationFor(params, this.session), signal);
|
||||
return {
|
||||
content: [{ type: "text", text: replaceTabs(toolContent(result, params)) }],
|
||||
details: toolDetails(result),
|
||||
details: await toolDetails(result, params),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -541,7 +562,10 @@ export const launchToolRenderer = {
|
||||
if (details?.timedOut) meta.push(theme.fg("warning", "follow timed out"));
|
||||
// Strip the trailing `[name: state; cursor=N]` status suffix `toolContent` appends.
|
||||
const logText = text.replace(/\n?\[[^\n]*\]$/, "").trimEnd();
|
||||
if (logText) {
|
||||
const terminalRows = details?.terminalRows;
|
||||
if (terminalRows) {
|
||||
for (const row of terminalRows) body.push(styleTerminalRow(row, theme.getFgAnsi("toolOutput")));
|
||||
} else if (logText) {
|
||||
for (const line of logText.split("\n")) body.push(theme.fg("toolOutput", replaceTabs(line)));
|
||||
}
|
||||
break;
|
||||
@@ -580,13 +604,32 @@ export const launchToolRenderer = {
|
||||
theme,
|
||||
);
|
||||
|
||||
if (op === "logs") {
|
||||
return framedBlock(theme, width => {
|
||||
const innerWidth = outputBlockContentWidth(width);
|
||||
const rows = body.map(line => truncateToWidth(line, innerWidth));
|
||||
return {
|
||||
header,
|
||||
state: options.isPartial ? "pending" : failed ? "error" : "success",
|
||||
sections: [
|
||||
{
|
||||
label: theme.fg("toolTitle", "Output"),
|
||||
lines: capPreviewLines(rows, theme, {
|
||||
expanded: options.expanded,
|
||||
max: DEFAULT_TERMINAL_PREVIEW_LINES,
|
||||
}),
|
||||
},
|
||||
],
|
||||
width,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return createCachedComponent(
|
||||
() => options.expanded,
|
||||
(width, expanded) => {
|
||||
let visible = body;
|
||||
if (op === "logs") {
|
||||
visible = capPreviewLines(body, theme, { expanded });
|
||||
} else if (!expanded && op === "list" && body.length > PREVIEW_LIMITS.COLLAPSED_ITEMS) {
|
||||
if (!expanded && op === "list" && body.length > PREVIEW_LIMITS.COLLAPSED_ITEMS) {
|
||||
const remaining = body.length - PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
||||
visible = [
|
||||
...body.slice(0, PREVIEW_LIMITS.COLLAPSED_ITEMS),
|
||||
|
||||
@@ -62,6 +62,9 @@ export const PREVIEW_LIMITS = {
|
||||
DIFF_COLLAPSED_LINES: 40,
|
||||
} as const;
|
||||
|
||||
/** Default number of terminal output rows shown before expansion. */
|
||||
export const DEFAULT_TERMINAL_PREVIEW_LINES = 10;
|
||||
|
||||
/** Truncation lengths for different content types */
|
||||
export const TRUNCATE_LENGTHS = {
|
||||
/** Short titles, labels */
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
||||
import type { Terminal as XtermTerminal } from "@xterm/headless";
|
||||
|
||||
const RESET = "\x1b[0m";
|
||||
const SGR = /\x1b\[([0-9;]*)m/g;
|
||||
|
||||
interface TerminalCell {
|
||||
getChars(): string;
|
||||
getWidth(): number;
|
||||
getFgColor(): number;
|
||||
getBgColor(): number;
|
||||
isBold(): number;
|
||||
isDim(): number;
|
||||
isItalic(): number;
|
||||
isUnderline(): number;
|
||||
isInverse(): number;
|
||||
isStrikethrough(): number;
|
||||
isOverline(): number;
|
||||
isFgRGB(): boolean;
|
||||
isBgRGB(): boolean;
|
||||
isFgPalette(): boolean;
|
||||
isBgPalette(): boolean;
|
||||
}
|
||||
|
||||
function addColor(codes: number[], cell: TerminalCell, foreground: boolean): void {
|
||||
const rgb = foreground ? cell.isFgRGB() : cell.isBgRGB();
|
||||
const palette = foreground ? cell.isFgPalette() : cell.isBgPalette();
|
||||
if (!rgb && !palette) return;
|
||||
|
||||
const color = foreground ? cell.getFgColor() : cell.getBgColor();
|
||||
codes.push(foreground ? 38 : 48);
|
||||
if (rgb) {
|
||||
codes.push(2, (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff);
|
||||
} else {
|
||||
codes.push(5, color);
|
||||
}
|
||||
}
|
||||
|
||||
function cellStyle(cell: TerminalCell): string {
|
||||
const codes: number[] = [];
|
||||
if (cell.isBold() !== 0) codes.push(1);
|
||||
if (cell.isDim() !== 0) codes.push(2);
|
||||
if (cell.isItalic() !== 0) codes.push(3);
|
||||
if (cell.isUnderline() !== 0) codes.push(4);
|
||||
if (cell.isInverse() !== 0) codes.push(7);
|
||||
if (cell.isStrikethrough() !== 0) codes.push(9);
|
||||
if (cell.isOverline() !== 0) codes.push(53);
|
||||
addColor(codes, cell, true);
|
||||
addColor(codes, cell, false);
|
||||
return codes.length > 0 ? `\x1b[${codes.join(";")}m` : "";
|
||||
}
|
||||
|
||||
function isSafeStyle(codes: readonly number[]): boolean {
|
||||
let index = 0;
|
||||
while (index < codes.length) {
|
||||
const code = codes[index++];
|
||||
if (code === 1 || code === 2 || code === 3 || code === 4 || code === 7 || code === 9 || code === 53) continue;
|
||||
if (code !== 38 && code !== 48) return false;
|
||||
const mode = codes[index++];
|
||||
if (mode === 5) {
|
||||
const color = codes[index++];
|
||||
if (color === undefined || color < 0 || color > 255) return false;
|
||||
continue;
|
||||
}
|
||||
if (mode !== 2) return false;
|
||||
for (let channel = 0; channel < 3; channel++) {
|
||||
const color = codes[index++];
|
||||
if (color === undefined || color < 0 || color > 255) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Applies the active tool-output color while preserving safe styles from a virtual terminal row. */
|
||||
export function styleTerminalRow(row: string, baseForeground: string): string {
|
||||
let output = baseForeground;
|
||||
let offset = 0;
|
||||
let hasText = false;
|
||||
for (const match of row.matchAll(SGR)) {
|
||||
const index = match.index ?? 0;
|
||||
const text = sanitizeText(row.slice(offset, index));
|
||||
output += text;
|
||||
hasText ||= text.length > 0;
|
||||
|
||||
const codes = match[1].split(";").map(Number);
|
||||
if (match[1] === "0") output += `${RESET}${baseForeground}`;
|
||||
else if (codes.length > 0 && codes.every(Number.isInteger) && isSafeStyle(codes)) output += match[0];
|
||||
offset = index + match[0].length;
|
||||
}
|
||||
const text = sanitizeText(row.slice(offset));
|
||||
output += text;
|
||||
hasText ||= text.length > 0;
|
||||
return hasText ? `${output}${RESET}` : "";
|
||||
}
|
||||
|
||||
/** Reads terminal screen rows as sanitized text plus only the styles the TUI may replay. */
|
||||
export function readTerminalRows(terminal: XtermTerminal, startRow: number, rowCount: number): string[] {
|
||||
const buffer = terminal.buffer.active;
|
||||
const reusableCell = buffer.getNullCell();
|
||||
const rows: string[] = [];
|
||||
const endRow = Math.min(buffer.length, Math.max(0, startRow) + Math.max(0, rowCount));
|
||||
|
||||
for (let row = Math.max(0, startRow); row < endRow; row++) {
|
||||
const line = buffer.getLine(row);
|
||||
if (!line) {
|
||||
rows.push("");
|
||||
continue;
|
||||
}
|
||||
|
||||
const cells: Array<{ chars: string; style: string }> = [];
|
||||
let lastContent = -1;
|
||||
for (let column = 0; column < line.length; ) {
|
||||
const cell = line.getCell(column, reusableCell);
|
||||
if (!cell) break;
|
||||
const chars = cell.getChars();
|
||||
const width = Math.max(1, cell.getWidth());
|
||||
cells.push({ chars: chars || " ", style: cellStyle(cell) });
|
||||
if (chars && chars !== " ") lastContent = cells.length - 1;
|
||||
column += width;
|
||||
}
|
||||
|
||||
if (lastContent < 0) {
|
||||
rows.push("");
|
||||
continue;
|
||||
}
|
||||
|
||||
let rendered = "";
|
||||
let previousStyle: string | undefined;
|
||||
for (let index = 0; index <= lastContent; index++) {
|
||||
const cell = cells[index]!;
|
||||
if (cell.style !== previousStyle) {
|
||||
rendered += `${RESET}${cell.style}`;
|
||||
previousStyle = cell.style;
|
||||
}
|
||||
rendered += cell.chars;
|
||||
}
|
||||
rows.push(rendered);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
@@ -70,10 +70,10 @@ export class SpeechEnhancer {
|
||||
async rewrite(block: string, signal?: AbortSignal): Promise<string | null> {
|
||||
try {
|
||||
const { settings, registry, sessionId } = this.#deps;
|
||||
// `pi/tiny` expands a configured `modelRoles.tiny` and otherwise falls
|
||||
// `@tiny` expands a configured `modelRoles.tiny` and otherwise falls
|
||||
// through tiny's alias to the smol priority chain — unlike bare role
|
||||
// lookup, this resolves even with no roles configured.
|
||||
const model = resolveModelRoleValue("pi/tiny", registry.getAvailable(), {
|
||||
const model = resolveModelRoleValue("@tiny", registry.getAvailable(), {
|
||||
settings,
|
||||
matchPreferences: getModelMatchPreferences(settings),
|
||||
}).model;
|
||||
|
||||
@@ -97,7 +97,7 @@ function formatImageBlock(localUrl: string, description: string): string {
|
||||
|
||||
/**
|
||||
* Resolve a vision-capable model, mirroring the inspect_image priority
|
||||
* (`pi/vision` → `pi/default` → active → first image-capable available), but
|
||||
* (`@vision` → `@default` → active → first image-capable available), but
|
||||
* never returning a text-only model.
|
||||
*/
|
||||
function resolveVisionModel(deps: DescribeAttachedImagesDeps): Model<Api> | undefined {
|
||||
@@ -111,8 +111,8 @@ function resolveVisionModel(deps: DescribeAttachedImagesDeps): Model<Api> | unde
|
||||
return model?.input.includes("image") ? model : undefined;
|
||||
};
|
||||
return (
|
||||
resolvePattern("pi/vision") ??
|
||||
resolvePattern("pi/default") ??
|
||||
resolvePattern("@vision") ??
|
||||
resolvePattern("@default") ??
|
||||
resolvePattern(deps.activeModelString) ??
|
||||
available.find(model => model.input.includes("image"))
|
||||
);
|
||||
|
||||
@@ -39,8 +39,8 @@ export type VibeCli = "fast" | "good";
|
||||
|
||||
/**
|
||||
* CLI flavor → bundled agent type. This IS the model-tier mapping: `sonic`
|
||||
* carries `model: "pi/smol"` (the configured fast/low-latency role) and `task`
|
||||
* carries `model: "pi/task"` (inherits the session's strong model).
|
||||
* carries `model: "@smol"` (the configured fast/low-latency role) and `task`
|
||||
* carries `model: "@task"` (inherits the session's strong model).
|
||||
* Resolution goes through {@link resolveAgentModelPatterns} exactly like a
|
||||
* `task` spawn, so `task.agentModelOverrides` and model-role settings apply.
|
||||
*/
|
||||
|
||||
@@ -2244,6 +2244,13 @@ describe("ACP agent", () => {
|
||||
return { connection, calls };
|
||||
}
|
||||
|
||||
/** Narrows `CreateElicitationRequest` to the `mode: "form"` branch; the SDK's `mode: string` catch-all arm otherwise defeats literal narrowing on `mode !== "form"`. */
|
||||
function isFormElicitation(
|
||||
request: CreateElicitationRequest,
|
||||
): request is Extract<CreateElicitationRequest, { mode: "form" }> {
|
||||
return request.mode === "form";
|
||||
}
|
||||
|
||||
it("translates select to a single-property string-enum elicitation", async () => {
|
||||
const { connection, calls } = createElicitConnection(async () => ({
|
||||
action: "accept",
|
||||
@@ -2258,7 +2265,7 @@ describe("ACP agent", () => {
|
||||
const request = calls[0]!;
|
||||
expect(request.mode).toBe("form");
|
||||
expect(request.message).toBe("Pick one");
|
||||
if (request.mode !== "form" || !("sessionId" in request)) {
|
||||
if (!isFormElicitation(request) || !("sessionId" in request)) {
|
||||
throw new Error("expected session-scoped form elicitation");
|
||||
}
|
||||
expect(request.sessionId).toBe("session-select");
|
||||
@@ -2281,7 +2288,7 @@ describe("ACP agent", () => {
|
||||
expect(result).toBe(true);
|
||||
expect(calls).toHaveLength(1);
|
||||
const request = calls[0]!;
|
||||
if (request.mode !== "form") {
|
||||
if (!isFormElicitation(request)) {
|
||||
throw new Error("expected form-mode elicitation");
|
||||
}
|
||||
expect(request.message).toBe("Proceed?\n\nThis will overwrite the file.");
|
||||
@@ -2301,7 +2308,7 @@ describe("ACP agent", () => {
|
||||
expect(result).toBe("claude");
|
||||
expect(calls).toHaveLength(1);
|
||||
const request = calls[0]!;
|
||||
if (request.mode !== "form") {
|
||||
if (!isFormElicitation(request)) {
|
||||
throw new Error("expected form-mode elicitation");
|
||||
}
|
||||
expect(request.message).toBe("Your name?");
|
||||
@@ -2450,7 +2457,7 @@ describe("ACP agent", () => {
|
||||
|
||||
expect(calls).toHaveLength(1);
|
||||
const request = calls[0]!;
|
||||
if (request.mode !== "form") throw new Error("expected form-mode elicitation");
|
||||
if (!isFormElicitation(request)) throw new Error("expected form-mode elicitation");
|
||||
expect(request.requestedSchema.properties?.value).toEqual({ type: "string" });
|
||||
});
|
||||
|
||||
|
||||
+13
-13
@@ -13,21 +13,21 @@ import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manage
|
||||
import { TempDir } from "@oh-my-pi/pi-utils";
|
||||
|
||||
/**
|
||||
* Downshift: one-way switch from the starting model to a fast/cheap target
|
||||
* Prewalk: one-way switch from the starting model to a fast/cheap target
|
||||
* at the first completed turn that starts execution — an edit/write tool,
|
||||
* or the todo-list init the plan nudge asks for — with a hidden plan nudge
|
||||
* before the switch and a hidden verify-before-finishing checklist after
|
||||
* it. This is the single mechanism that won out over fixed-turn and
|
||||
* ungated variants in benchmark testing — see the plan nudge / checklist /
|
||||
* continuation-safety-net prompts under `src/prompts/system/downshift-*.md`.
|
||||
* continuation-safety-net prompts under `src/prompts/system/prewalk-*.md`.
|
||||
*/
|
||||
describe("AgentSession downshift", () => {
|
||||
describe("AgentSession prewalk", () => {
|
||||
let tempDir: TempDir;
|
||||
let authStorage: AuthStorage;
|
||||
let session: AgentSession | undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = TempDir.createSync("@pi-downshift-");
|
||||
tempDir = TempDir.createSync("@pi-prewalk-");
|
||||
authStorage = await AuthStorage.create(path.join(tempDir.path(), "auth.db"));
|
||||
authStorage.setRuntimeApiKey("anthropic", "test-key");
|
||||
});
|
||||
@@ -110,7 +110,7 @@ describe("AgentSession downshift", () => {
|
||||
});
|
||||
}
|
||||
|
||||
it("downshifts at the first edit/write after the todo gate opens; bash and todo don't trigger", async () => {
|
||||
it("prewalks at the first edit/write after the todo gate opens; bash and todo don't trigger", async () => {
|
||||
const primary = modelOrThrow("claude-sonnet-4-5");
|
||||
const target = modelOrThrow("claude-sonnet-4-6");
|
||||
const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir.path(), "models.yml"));
|
||||
@@ -155,7 +155,7 @@ describe("AgentSession downshift", () => {
|
||||
settings: Settings.isolated({ "compaction.enabled": false }),
|
||||
modelRegistry,
|
||||
toolRegistry,
|
||||
downshift: { target },
|
||||
prewalk: { target },
|
||||
});
|
||||
|
||||
await session.prompt("do the task");
|
||||
@@ -217,7 +217,7 @@ describe("AgentSession downshift", () => {
|
||||
settings: Settings.isolated({ "compaction.enabled": false }),
|
||||
modelRegistry,
|
||||
toolRegistry,
|
||||
downshift: { target },
|
||||
prewalk: { target },
|
||||
});
|
||||
|
||||
await session.prompt("do the task");
|
||||
@@ -277,7 +277,7 @@ describe("AgentSession downshift", () => {
|
||||
[recordTool.name, recordTool as AgentTool],
|
||||
[writeTool.name, writeTool as AgentTool],
|
||||
]),
|
||||
downshift: { target },
|
||||
prewalk: { target },
|
||||
});
|
||||
|
||||
await session.prompt("do the task");
|
||||
@@ -292,13 +292,13 @@ describe("AgentSession downshift", () => {
|
||||
expect(session.model?.id).toBe(target.id);
|
||||
});
|
||||
|
||||
it("armDownshift (the /downshift slash command) pre-arms the switch for the very next edit/write", async () => {
|
||||
it("armPrewalk (the /prewalk slash command) pre-arms the switch for the very next edit/write", async () => {
|
||||
const primary = modelOrThrow("claude-sonnet-4-5");
|
||||
const target = modelOrThrow("claude-sonnet-4-6");
|
||||
const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir.path(), "models.yml"));
|
||||
|
||||
// No `downshift` in the session config — this simulates a session that
|
||||
// was NOT started with --downshift, forced on via the slash command.
|
||||
// No `prewalk` in the session config — this simulates a session that
|
||||
// was NOT started with --prewalk, forced on via the slash command.
|
||||
const mock = createMockModel({ responses: [toolCall("t1", "write"), { content: ["done"] }] });
|
||||
const requested: string[] = [];
|
||||
const agent = new Agent({
|
||||
@@ -325,8 +325,8 @@ describe("AgentSession downshift", () => {
|
||||
});
|
||||
|
||||
// Arming twice back-to-back must stay a single, idempotent arm.
|
||||
session.armDownshift(target);
|
||||
session.armDownshift(target);
|
||||
session.armPrewalk(target);
|
||||
session.armPrewalk(target);
|
||||
|
||||
await session.prompt("do the task");
|
||||
|
||||
@@ -114,7 +114,7 @@ describe("AgentSession per-turn prune persistence", () => {
|
||||
const message = session.agent.state.messages.find(
|
||||
candidate => candidate.role === "toolResult" && candidate.toolCallId === BIG_CALL_ID,
|
||||
);
|
||||
if (!message || message.role !== "toolResult" || !Array.isArray(message.content)) {
|
||||
if (message?.role !== "toolResult" || !Array.isArray(message.content)) {
|
||||
throw new Error("Expected the seeded tool result in live agent state");
|
||||
}
|
||||
const text = message.content.find(block => block.type === "text");
|
||||
@@ -156,7 +156,7 @@ describe("AgentSession per-turn prune persistence", () => {
|
||||
const rebuilt = reloaded
|
||||
.buildSessionContext()
|
||||
.messages.find(candidate => candidate.role === "toolResult" && candidate.toolCallId === BIG_CALL_ID);
|
||||
if (!rebuilt || rebuilt.role !== "toolResult" || !Array.isArray(rebuilt.content)) {
|
||||
if (rebuilt?.role !== "toolResult" || !Array.isArray(rebuilt.content)) {
|
||||
throw new Error("Expected the seeded tool result in the from-disk rebuild");
|
||||
}
|
||||
const rebuiltText = rebuilt.content.find(block => block.type === "text");
|
||||
|
||||
@@ -12,7 +12,7 @@ describe("bundled agent parsing", () => {
|
||||
|
||||
expect(reviewer).toBeDefined();
|
||||
expect(reviewer?.source).toBe("bundled");
|
||||
expect(reviewer?.model).toEqual(["pi/slow"]);
|
||||
expect(reviewer?.model).toEqual(["@slow"]);
|
||||
expect(reviewer?.thinkingLevel).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ describe("bundled agent parsing", () => {
|
||||
const task = getBundledAgent("task");
|
||||
|
||||
expect(task).toBeDefined();
|
||||
expect(task?.model).toEqual(["pi/task"]);
|
||||
expect(task?.model).toEqual(["@task"]);
|
||||
expect(task?.thinkingLevel).toBe(AUTO_THINKING);
|
||||
});
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("commit role thinking selection", () => {
|
||||
const settings = createSettings({
|
||||
default: `${defaultModel.provider}/${defaultModel.id}:high`,
|
||||
commit: `${commitModel.provider}/${commitModel.id}:low`,
|
||||
smol: "pi/default:minimal",
|
||||
smol: "@default:minimal",
|
||||
});
|
||||
const registry = {
|
||||
getAvailable: () => [defaultModel, commitModel],
|
||||
|
||||
@@ -369,7 +369,7 @@ describe("hashline — anchor-stale recovery via read snapshot cache", () => {
|
||||
expect(finalLines).toContain("L8");
|
||||
|
||||
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
|
||||
expect(text).toMatch(/Recovered from a stale file hash using a previous read snapshot/);
|
||||
expect(text).toMatch(/Recovered by remapping stale line anchors to unchanged current lines/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -437,7 +437,7 @@ describe("hashline — anchor-stale recovery via read snapshot cache", () => {
|
||||
expect(finalLines).toContain("GAMMA");
|
||||
expect(finalLines).not.toContain("gamma");
|
||||
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
|
||||
expect(text).toMatch(/Recovered from a stale file hash using a previous read snapshot/);
|
||||
expect(text).toMatch(/Recovered by remapping stale line anchors to unchanged current lines/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ describe("createExtensionModelQuery", () => {
|
||||
getModelRole: (role: string) => (role === "slow" ? "anthropic/claude-opus-4-8" : undefined),
|
||||
} as unknown as Settings;
|
||||
const q = createExtensionModelQuery(registry(), settings, () => undefined);
|
||||
expect(q.resolve("pi/slow")).toBe(claude);
|
||||
expect(q.resolve("@slow")).toBe(claude);
|
||||
});
|
||||
|
||||
test("family() groups a vendor's point releases and separates vendors", () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { beforeAll, describe, expect, test, vi } from "bun:test";
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import { resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
|
||||
import type { InteractiveModeContext } from "@oh-my-pi/pi-coding-agent/modes/types";
|
||||
import { UiHelpers } from "@oh-my-pi/pi-coding-agent/modes/utils/ui-helpers";
|
||||
@@ -64,7 +65,10 @@ function createInitialRenderHarness(): { ctx: InteractiveModeContext; helpers: U
|
||||
|
||||
describe("InteractiveMode.showStatus", () => {
|
||||
beforeAll(async () => {
|
||||
// showStatus uses the global theme instance
|
||||
// showStatus uses the global theme instance; renderInitialMessages reads
|
||||
// the global Settings (display.collapseCompacted).
|
||||
resetSettingsForTest();
|
||||
await Settings.init({ inMemory: true });
|
||||
await initTheme();
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
resolveModelRoleValue,
|
||||
resolveModelScope,
|
||||
} from "@oh-my-pi/pi-coding-agent/config/model-resolver";
|
||||
import { DEFAULT_MODEL_ROLE_ALIAS, LEGACY_MODEL_ROLE_ALIAS_PREFIX } from "@oh-my-pi/pi-coding-agent/config/model-roles";
|
||||
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
|
||||
// Mock models for testing
|
||||
@@ -623,12 +624,12 @@ describe("parseModelPattern", () => {
|
||||
});
|
||||
|
||||
describe("resolveModelRoleValue", () => {
|
||||
test("resolves pi/<role>:<thinking> by expanding role alias before parsing thinking", () => {
|
||||
test("resolves @role:<thinking> by expanding role alias before parsing thinking", () => {
|
||||
const settings = {
|
||||
getModelRole: (role: string) => (role === "smol" ? "openrouter/qwen/qwen3-coder:exacto" : undefined),
|
||||
} as NonNullable<Parameters<typeof resolveModelRoleValue>[2]>["settings"];
|
||||
|
||||
const result = resolveModelRoleValue("pi/smol:high", allModels, { settings });
|
||||
const result = resolveModelRoleValue("@smol:high", allModels, { settings });
|
||||
|
||||
expect(result.model?.provider).toBe("openrouter");
|
||||
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
|
||||
@@ -636,12 +637,12 @@ describe("resolveModelRoleValue", () => {
|
||||
expect(result.explicitThinkingLevel).toBe(true);
|
||||
});
|
||||
|
||||
test("resolves pi/<role>:max by expanding role alias before parsing thinking", () => {
|
||||
test("resolves @role:max by expanding role alias before parsing thinking", () => {
|
||||
const settings = {
|
||||
getModelRole: (role: string) => (role === "smol" ? "openai-codex/gpt-5.3-codex" : undefined),
|
||||
} as NonNullable<Parameters<typeof resolveModelRoleValue>[2]>["settings"];
|
||||
|
||||
const result = resolveModelRoleValue("pi/smol:max", allModels, { settings });
|
||||
const result = resolveModelRoleValue("@smol:max", allModels, { settings });
|
||||
|
||||
expect(result.model?.provider).toBe("openai-codex");
|
||||
expect(result.model?.id).toBe("gpt-5.3-codex");
|
||||
@@ -650,12 +651,12 @@ describe("resolveModelRoleValue", () => {
|
||||
expect(result.explicitThinkingLevel).toBe(true);
|
||||
});
|
||||
|
||||
test("resolves pi/default through configured default role alias", () => {
|
||||
test("resolves @default through configured default role alias", () => {
|
||||
const settings = {
|
||||
getModelRole: (role: string) => (role === "default" ? "openrouter/qwen/qwen3-coder:exacto" : undefined),
|
||||
} as NonNullable<Parameters<typeof resolveModelRoleValue>[2]>["settings"];
|
||||
|
||||
const result = resolveModelRoleValue("pi/default", allModels, { settings });
|
||||
const result = resolveModelRoleValue("@default", allModels, { settings });
|
||||
|
||||
expect(result.model?.provider).toBe("openrouter");
|
||||
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
|
||||
@@ -736,13 +737,13 @@ describe("resolveModelRoleValue", () => {
|
||||
});
|
||||
});
|
||||
describe("resolveAgentModelPatterns", () => {
|
||||
test("falls back to the active session model when pi/task is unset", () => {
|
||||
test("falls back to the active session model when @task is unset", () => {
|
||||
const settings = Settings.isolated({
|
||||
modelRoles: { default: "anthropic/claude-sonnet-4-5" },
|
||||
});
|
||||
|
||||
const result = resolveAgentModelPatterns({
|
||||
agentModel: "pi/task",
|
||||
agentModel: "@task",
|
||||
settings,
|
||||
activeModelPattern: "openai/gpt-4o",
|
||||
});
|
||||
@@ -759,7 +760,7 @@ describe("resolveAgentModelPatterns", () => {
|
||||
});
|
||||
|
||||
const result = resolveAgentModelPatterns({
|
||||
agentModel: "pi/task",
|
||||
agentModel: "@task",
|
||||
settings,
|
||||
activeModelPattern: "openai/gpt-4o",
|
||||
});
|
||||
@@ -775,7 +776,7 @@ describe("resolveAgentModelPatterns", () => {
|
||||
});
|
||||
|
||||
const result = resolveAgentModelPatterns({
|
||||
agentModel: "pi/task",
|
||||
agentModel: "@task",
|
||||
settings,
|
||||
});
|
||||
|
||||
@@ -787,17 +788,17 @@ describe("resolveAgentModelPatterns", () => {
|
||||
modelRoles: { default: "local/llama" },
|
||||
});
|
||||
|
||||
expect(resolveAgentModelPatterns({ agentModel: "pi/smol", settings })).toEqual(["local/llama"]);
|
||||
expect(resolveAgentModelPatterns({ agentModel: "pi/slow", settings })).toEqual(["local/llama"]);
|
||||
expect(resolveAgentModelPatterns({ agentModel: "pi/designer", settings })).toEqual(["local/llama"]);
|
||||
expect(resolveAgentModelPatterns({ agentModel: "@smol", settings })).toEqual(["local/llama"]);
|
||||
expect(resolveAgentModelPatterns({ agentModel: "@slow", settings })).toEqual(["local/llama"]);
|
||||
expect(resolveAgentModelPatterns({ agentModel: "@designer", settings })).toEqual(["local/llama"]);
|
||||
});
|
||||
|
||||
test("expands cross-role default aliases when inheriting for an unset role", () => {
|
||||
const settings = Settings.isolated({
|
||||
modelRoles: { default: "pi/slow", slow: "anthropic/claude-sonnet-4-5" },
|
||||
modelRoles: { default: "@slow", slow: "anthropic/claude-sonnet-4-5" },
|
||||
});
|
||||
|
||||
expect(resolveAgentModelPatterns({ agentModel: "pi/smol", settings })).toEqual(["anthropic/claude-sonnet-4-5"]);
|
||||
expect(resolveAgentModelPatterns({ agentModel: "@smol", settings })).toEqual(["anthropic/claude-sonnet-4-5"]);
|
||||
});
|
||||
|
||||
test("prefers configured designer role override over priority defaults", () => {
|
||||
@@ -809,7 +810,7 @@ describe("resolveAgentModelPatterns", () => {
|
||||
});
|
||||
|
||||
const result = resolveAgentModelPatterns({
|
||||
agentModel: "pi/designer",
|
||||
agentModel: "@designer",
|
||||
settings,
|
||||
});
|
||||
|
||||
@@ -818,7 +819,7 @@ describe("resolveAgentModelPatterns", () => {
|
||||
|
||||
test("slow priority falls forward to Opus 4.8 before older Opus aliases", () => {
|
||||
const settings = Settings.isolated();
|
||||
const patterns = resolveAgentModelPatterns({ agentModel: "pi/slow", settings });
|
||||
const patterns = resolveAgentModelPatterns({ agentModel: "@slow", settings });
|
||||
|
||||
const dottedRegistry = {
|
||||
getAvailable: () => [
|
||||
@@ -898,6 +899,70 @@ describe("resolveCliModel", () => {
|
||||
expect(result.model?.id).toBe("gpt-4o");
|
||||
});
|
||||
|
||||
test("resolves configured custom, legacy, and default role aliases from --model", () => {
|
||||
const registry = {
|
||||
getAll: () => allModels,
|
||||
};
|
||||
const settings = Settings.isolated({
|
||||
modelRoles: {
|
||||
default: "openai/gpt-4o",
|
||||
fable: "anthropic/claude-sonnet-4-5:high",
|
||||
},
|
||||
});
|
||||
|
||||
const canonical = resolveCliModel({
|
||||
cliModel: "@fable",
|
||||
modelRegistry: registry,
|
||||
settings,
|
||||
});
|
||||
const legacy = resolveCliModel({
|
||||
cliModel: `${LEGACY_MODEL_ROLE_ALIAS_PREFIX}fable`,
|
||||
modelRegistry: registry,
|
||||
settings,
|
||||
});
|
||||
const defaultRole = resolveCliModel({
|
||||
cliModel: DEFAULT_MODEL_ROLE_ALIAS,
|
||||
modelRegistry: registry,
|
||||
settings,
|
||||
});
|
||||
|
||||
expect(canonical.error).toBeUndefined();
|
||||
expect(canonical.model?.provider).toBe("anthropic");
|
||||
expect(canonical.model?.id).toBe("claude-sonnet-4-5");
|
||||
expect(canonical.thinkingLevel).toBe(Effort.High);
|
||||
expect(legacy).toEqual(canonical);
|
||||
expect(defaultRole.model?.provider).toBe("openai");
|
||||
expect(defaultRole.model?.id).toBe("gpt-4o");
|
||||
});
|
||||
|
||||
test("splits thinking suffixes and abbreviations off the * default alias", () => {
|
||||
const registry = {
|
||||
getAll: () => allModels,
|
||||
};
|
||||
const settings = Settings.isolated({
|
||||
modelRoles: { default: "anthropic/claude-sonnet-4-5" },
|
||||
});
|
||||
|
||||
const explicit = resolveCliModel({
|
||||
cliModel: `${DEFAULT_MODEL_ROLE_ALIAS}:high`,
|
||||
modelRegistry: registry,
|
||||
settings,
|
||||
});
|
||||
const abbreviated = resolveCliModel({
|
||||
cliModel: `${DEFAULT_MODEL_ROLE_ALIAS}:xhi`,
|
||||
modelRegistry: registry,
|
||||
settings,
|
||||
});
|
||||
|
||||
expect(explicit.error).toBeUndefined();
|
||||
expect(explicit.model?.id).toBe("claude-sonnet-4-5");
|
||||
expect(explicit.thinkingLevel).toBe(Effort.High);
|
||||
// `xhi` → xhigh via unique-prefix parsing, then clamped to the model ladder.
|
||||
expect(abbreviated.error).toBeUndefined();
|
||||
expect(abbreviated.model?.id).toBe("claude-sonnet-4-5");
|
||||
expect(abbreviated.thinkingLevel).toBe(Effort.High);
|
||||
});
|
||||
|
||||
test("resolves fuzzy patterns within an explicit provider", () => {
|
||||
const registry = {
|
||||
getAll: () => allModels,
|
||||
@@ -1108,6 +1173,25 @@ describe("resolveModelScope", () => {
|
||||
expect(scoped[0].model.id).toBe("gpt-5.5");
|
||||
});
|
||||
|
||||
test("resolves role aliases in --models scope to the role's model with its thinking level", async () => {
|
||||
const settings = Settings.isolated({
|
||||
modelRoles: { fable: "anthropic/claude-sonnet-4-5:high" },
|
||||
});
|
||||
|
||||
const scoped = await resolveModelScope(
|
||||
["@fable", "openai/gpt-4o"],
|
||||
{ getAvailable: () => allModels },
|
||||
undefined,
|
||||
settings,
|
||||
);
|
||||
|
||||
expect(scoped).toHaveLength(2);
|
||||
expect(scoped[0].model.id).toBe("claude-sonnet-4-5");
|
||||
expect(scoped[0].thinkingLevel).toBe(Effort.High);
|
||||
expect(scoped[0].explicitThinkingLevel).toBe(true);
|
||||
expect(scoped[1].model.id).toBe("gpt-4o");
|
||||
});
|
||||
|
||||
test("applies max thinking selectors to glob scopes when no literal max ids match", async () => {
|
||||
const registry = {
|
||||
getAvailable: () => mockCodexOverlapModels,
|
||||
@@ -1272,18 +1356,18 @@ describe("resolveModelFromString", () => {
|
||||
});
|
||||
|
||||
describe("expandRoleAlias", () => {
|
||||
test("expands pi/vision to configured vision role", () => {
|
||||
test("expands @vision to configured vision role", () => {
|
||||
const settings = Settings.isolated();
|
||||
settings.setModelRole("vision", "openai/gpt-4o");
|
||||
|
||||
expect(expandRoleAlias("pi/vision", settings)).toBe("openai/gpt-4o");
|
||||
expect(expandRoleAlias("@vision", settings)).toBe("openai/gpt-4o");
|
||||
});
|
||||
|
||||
test("keeps pi/vision alias when vision role is unset", () => {
|
||||
test("keeps @vision alias when vision role is unset", () => {
|
||||
const settings = Settings.isolated();
|
||||
settings.setModelRole("default", "anthropic/claude-sonnet-4-5");
|
||||
|
||||
expect(expandRoleAlias("pi/vision", settings)).toBe("pi/vision");
|
||||
expect(expandRoleAlias("@vision", settings)).toBe("@vision");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1305,7 +1389,7 @@ describe("extractExplicitThinkingSelector", () => {
|
||||
test("treats max on pi role aliases as an explicit selector before expansion", () => {
|
||||
const settings = Settings.isolated();
|
||||
settings.setModelRole("smol", "nanogpt/coding-router:max");
|
||||
const result = extractExplicitThinkingSelector("pi/smol:max", settings, {
|
||||
const result = extractExplicitThinkingSelector("@smol:max", settings, {
|
||||
isLiteralModelId: (provider, id) => provider === "nanogpt" && id === "coding-router:max",
|
||||
});
|
||||
expect(result).toBe(Effort.Max);
|
||||
@@ -1442,6 +1526,15 @@ describe("filterAvailableModelsByEnabledPatterns", () => {
|
||||
expect(filterAvailableModelsByEnabledPatterns(models, [])).toEqual(models);
|
||||
});
|
||||
|
||||
test("resolves role aliases to the role's model when settings are provided", () => {
|
||||
const settings = Settings.isolated({
|
||||
modelRoles: { fable: "anthropic/claude-sonnet-4-5:high" },
|
||||
});
|
||||
const result = filterAvailableModelsByEnabledPatterns(models, ["@fable"], settings);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe("claude-sonnet-4-5");
|
||||
});
|
||||
|
||||
test("filters by exact provider/modelId", () => {
|
||||
const result = filterAvailableModelsByEnabledPatterns(models, ["anthropic/claude-sonnet-4-5"]);
|
||||
expect(result).toHaveLength(1);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* scrollback-clearing repaint (`clearTerminalHistory`).
|
||||
*/
|
||||
|
||||
import { afterEach, beforeAll, describe, expect, it, type Mock, vi } from "bun:test";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, type Mock, vi } from "bun:test";
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import type { AssistantMessage, ImageContent, Usage } from "@oh-my-pi/pi-ai";
|
||||
import { kStreamingPartialJson } from "@oh-my-pi/pi-ai/utils/block-symbols";
|
||||
@@ -28,6 +28,13 @@ beforeAll(() => {
|
||||
initTheme();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// afterEach resets Settings, but renderInitialMessages reads the global
|
||||
// Settings (display.collapseCompacted) — re-init before every test.
|
||||
resetSettingsForTest();
|
||||
await Settings.init({ inMemory: true });
|
||||
});
|
||||
|
||||
const originalImageProtocol = TERMINAL.imageProtocol;
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { afterEach, beforeAll, describe, expect, test, vi } from "bun:test";
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
||||
import { resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import type {
|
||||
ExtensionActions,
|
||||
ExtensionCommandContextActions,
|
||||
@@ -31,6 +32,9 @@ import { Container } from "@oh-my-pi/pi-tui";
|
||||
* leaving two identical custom-message components in the chat.
|
||||
*/
|
||||
beforeAll(async () => {
|
||||
// renderInitialMessages reads the global Settings (display.collapseCompacted).
|
||||
resetSettingsForTest();
|
||||
await Settings.init({ inMemory: true });
|
||||
await initTheme();
|
||||
});
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ describe("role thinking helper propagation", () => {
|
||||
const model = getModelOrThrow("claude-sonnet-4-5");
|
||||
const settings = createSettings({
|
||||
default: `${model.provider}/${model.id}:high`,
|
||||
smol: "pi/default:minimal",
|
||||
smol: "@default:minimal",
|
||||
});
|
||||
const registry = {
|
||||
getAvailable: () => [model],
|
||||
@@ -85,7 +85,7 @@ describe("role thinking helper propagation", () => {
|
||||
const model = getModelOrThrow("claude-sonnet-4-5");
|
||||
const settings = createSettings({
|
||||
default: `${model.provider}/${model.id}:high`,
|
||||
smol: "pi/default:low",
|
||||
smol: "@default:low",
|
||||
});
|
||||
const registry = {
|
||||
getAvailable: () => [model],
|
||||
|
||||
@@ -213,7 +213,7 @@ describe("createAgentSession deferred model pattern resolution", () => {
|
||||
settings.setModelRole("smol", "runtime-provider/runtime-model");
|
||||
|
||||
const { session, modelFallbackMessage } = await createAgentSession({
|
||||
...(await buildSessionOptions("pi/smol")),
|
||||
...(await buildSessionOptions("@smol")),
|
||||
settings,
|
||||
});
|
||||
|
||||
@@ -265,7 +265,7 @@ describe("createAgentSession deferred model pattern resolution", () => {
|
||||
test("does not apply default role thinking override when modelPattern is explicit", async () => {
|
||||
const settings = Settings.isolated({ defaultThinkingLevel: "off" });
|
||||
settings.setModelRole("smol", "runtime-provider/runtime-reasoning-model");
|
||||
settings.setModelRole("default", "pi/smol:high");
|
||||
settings.setModelRole("default", "@smol:high");
|
||||
|
||||
const { session } = await createAgentSession({
|
||||
...(await buildSessionOptions("runtime-provider/runtime-reasoning-model")),
|
||||
|
||||
@@ -26,6 +26,7 @@ function createModelContext(advisorActive: boolean): SegmentContext {
|
||||
options: {},
|
||||
planMode: null,
|
||||
loopMode: null,
|
||||
prewalk: null,
|
||||
goalMode: null,
|
||||
vibeMode: null,
|
||||
collab: null,
|
||||
|
||||
@@ -45,6 +45,7 @@ function createCtx(overrides?: { pathMaxLength?: number; branch?: string | null
|
||||
},
|
||||
planMode: null,
|
||||
loopMode: null,
|
||||
prewalk: null,
|
||||
goalMode: null,
|
||||
vibeMode: null,
|
||||
collab: null,
|
||||
|
||||
@@ -31,6 +31,7 @@ function createPathContext(): SegmentContext {
|
||||
},
|
||||
planMode: null,
|
||||
loopMode: null,
|
||||
prewalk: null,
|
||||
goalMode: null,
|
||||
vibeMode: null,
|
||||
collab: null,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user