@code-yeongyu/senpi 2026.9.21-2 → 2026.9.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/bundle/chunks/{anthropic-messages-C3ZY2ANT.js → anthropic-messages-2P7APOOQ.js} +1 -1
- package/dist/bundle/chunks/{app-server-command-HSX5RFAP.js → app-server-command-DLBXJ5K5.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-7TAGS2VU.js → azure-openai-responses-3AH7MJWF.js} +1 -1
- package/dist/bundle/chunks/{chunk-3SLCHXZO.js → chunk-4AXZDO3E.js} +2 -2
- package/dist/bundle/chunks/{chunk-Z3KQB2R3.js → chunk-7ICH7UFW.js} +12 -12
- package/dist/bundle/chunks/{chunk-KNM6SRPE.js → chunk-AAVNXTGQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-IDTLJYJ5.js → chunk-AY37SZJZ.js} +4 -4
- package/dist/bundle/chunks/{chunk-XHMOFY4T.js → chunk-BUPX33Q4.js} +3 -3
- package/dist/bundle/chunks/{chunk-GLKVDRNP.js → chunk-CWCY7LXK.js} +1 -1
- package/dist/bundle/chunks/{chunk-POMPEGWQ.js → chunk-F3DXL5MX.js} +1 -1
- package/dist/bundle/chunks/{chunk-GVI2HP7N.js → chunk-I2HIIJPI.js} +1 -1
- package/dist/bundle/chunks/{chunk-T6XA2IZR.js → chunk-JR4X6ECO.js} +1 -1
- package/dist/bundle/chunks/{chunk-M2XAY6I7.js → chunk-M2HSO7MH.js} +1 -1
- package/dist/bundle/chunks/{chunk-MIVOQT3Q.js → chunk-N5AQJAQ2.js} +1 -1
- package/dist/bundle/chunks/{chunk-UYVPLEOL.js → chunk-NK2RNX25.js} +1 -1
- package/dist/bundle/chunks/chunk-NMB4C6ZZ.js +2 -0
- package/dist/bundle/chunks/{chunk-B7W62BM7.js → chunk-P4OHEJ7P.js} +1 -1
- package/dist/bundle/chunks/{chunk-I425CMTO.js → chunk-VBNHBSBD.js} +1 -1
- package/dist/bundle/chunks/{chunk-B2IM56LB.js → chunk-VYIJQQ5I.js} +1 -1
- package/dist/bundle/chunks/chunk-XRHUMMIW.js +8 -0
- package/dist/bundle/chunks/{chunk-47GF6XYZ.js → chunk-YT2ZSZOY.js} +1 -1
- package/dist/bundle/chunks/{chunk-OSDJ2SYO.js → chunk-ZCBX2K34.js} +1 -1
- package/dist/bundle/chunks/{chunk-D5CL7PPD.js → chunk-ZMVXBX33.js} +2 -2
- package/dist/bundle/chunks/{cli-main-GQTEEZ2V.js → cli-main-D2VPNIQ3.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-JWIH75VF.js → google-generative-ai-Y2IYMUVC.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-IRGAY5XR.js → google-vertex-TKBBMPHV.js} +1 -1
- package/dist/bundle/chunks/{host-command-KOEKIHIZ.js → host-command-IFIIOX5S.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle-3OJZA4QQ.js +3 -0
- package/dist/bundle/chunks/host-lifecycle.js +3 -3
- package/dist/bundle/chunks/{interactive-host-runtime-2KTFEW7O.js → interactive-host-runtime-V7PBDIOJ.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-I3ILWXQB.js → interactive-mode-RFMMGQCT.js} +1 -1
- package/dist/bundle/chunks/kimi-coding.js +2 -2
- package/dist/bundle/chunks/{mistral-conversations-VOBYATVJ.js → mistral-conversations-ZYDVFRYF.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-S5FWCIWE.js → multi-session-host-45YMCQIH.js} +8 -7
- package/dist/bundle/chunks/openai-codex-responses-MEIUNROM.js +10 -0
- package/dist/bundle/chunks/{openai-completions-YK6QTYOH.js → openai-completions-4V76L5ZJ.js} +1 -1
- package/dist/bundle/chunks/openai-responses-YR7WRBG3.js +2 -0
- package/dist/bundle/chunks/{package-manager-cli-HOL6CAI6.js → package-manager-cli-7DQKRSI5.js} +1 -1
- package/dist/bundle/chunks/rotation-stream-YPZP3VJS.js +2 -0
- package/dist/bundle/chunks/{rpc-mode-L3DSGTVM.js → rpc-mode-4GWZCJKI.js} +1 -1
- package/dist/bundle/chunks/{session-picker-L3FYEQBL.js → session-picker-ZKRYWZGC.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +38 -37
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/core/agent-session.d.ts +6 -4
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +9 -7
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/credential-pool/classify.d.ts.map +1 -1
- package/dist/core/credential-pool/classify.js +12 -0
- package/dist/core/credential-pool/classify.js.map +1 -1
- package/dist/core/credential-pool/failover.d.ts +37 -5
- package/dist/core/credential-pool/failover.d.ts.map +1 -1
- package/dist/core/credential-pool/failover.js +60 -24
- package/dist/core/credential-pool/failover.js.map +1 -1
- package/dist/core/credential-pool/rotation-events.d.ts +5 -0
- package/dist/core/credential-pool/rotation-events.d.ts.map +1 -0
- package/dist/core/credential-pool/rotation-events.js +25 -0
- package/dist/core/credential-pool/rotation-events.js.map +1 -0
- package/dist/core/credential-pool/rotation-stream.d.ts +4 -3
- package/dist/core/credential-pool/rotation-stream.d.ts.map +1 -1
- package/dist/core/credential-pool/rotation-stream.js +8 -11
- package/dist/core/credential-pool/rotation-stream.js.map +1 -1
- package/dist/core/extensions/builtin/ask-user/schema.d.ts +2 -25
- package/dist/core/extensions/builtin/ask-user/schema.d.ts.map +1 -1
- package/dist/core/extensions/builtin/ask-user/schema.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/deterministic-fallback.d.ts +2 -7
- package/dist/core/extensions/builtin/compaction/deterministic-fallback.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/deterministic-fallback.js +2 -8
- package/dist/core/extensions/builtin/compaction/deterministic-fallback.js.map +1 -1
- package/dist/core/extensions/builtin/mcp/expose/naming.d.ts +0 -1
- package/dist/core/extensions/builtin/mcp/expose/naming.d.ts.map +1 -1
- package/dist/core/extensions/builtin/mcp/expose/naming.js +0 -3
- package/dist/core/extensions/builtin/mcp/expose/naming.js.map +1 -1
- package/dist/core/extensions/builtin/mcp/expose/register.d.ts +1 -2
- package/dist/core/extensions/builtin/mcp/expose/register.d.ts.map +1 -1
- package/dist/core/extensions/builtin/mcp/expose/register.js +0 -21
- package/dist/core/extensions/builtin/mcp/expose/register.js.map +1 -1
- package/dist/core/extensions/builtin/mcp/expose/schema-compat.d.ts +2 -2
- package/dist/core/extensions/builtin/mcp/expose/schema-compat.d.ts.map +1 -1
- package/dist/core/extensions/builtin/mcp/expose/schema-compat.js +2 -2
- package/dist/core/extensions/builtin/mcp/expose/schema-compat.js.map +1 -1
- package/dist/core/http-dispatcher.d.ts +29 -0
- package/dist/core/http-dispatcher.d.ts.map +1 -1
- package/dist/core/http-dispatcher.js +30 -3
- package/dist/core/http-dispatcher.js.map +1 -1
- package/dist/modes/interactive/components/assistant-render-descriptors.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-render-descriptors.js +8 -7
- package/dist/modes/interactive/components/assistant-render-descriptors.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +7 -5
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/handoff-activity.d.ts +4 -0
- package/dist/modes/rpc/handoff-activity.d.ts.map +1 -0
- package/dist/modes/rpc/handoff-activity.js +6 -0
- package/dist/modes/rpc/handoff-activity.js.map +1 -0
- package/dist/modes/rpc/host-ensure.d.ts.map +1 -1
- package/dist/modes/rpc/host-ensure.js +60 -17
- package/dist/modes/rpc/host-ensure.js.map +1 -1
- package/dist/modes/rpc/host-lifecycle.d.ts +3 -0
- package/dist/modes/rpc/host-lifecycle.d.ts.map +1 -1
- package/dist/modes/rpc/host-lifecycle.js +25 -5
- package/dist/modes/rpc/host-lifecycle.js.map +1 -1
- package/dist/modes/rpc/multi-session-host.d.ts +1 -0
- package/dist/modes/rpc/multi-session-host.d.ts.map +1 -1
- package/dist/modes/rpc/multi-session-host.js +40 -7
- package/dist/modes/rpc/multi-session-host.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +13 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/modes/rpc/session-command-router.d.ts +10 -4
- package/dist/modes/rpc/session-command-router.d.ts.map +1 -1
- package/dist/modes/rpc/session-command-router.js +90 -18
- package/dist/modes/rpc/session-command-router.js.map +1 -1
- package/dist/modes/rpc/session-event-writer.d.ts +5 -3
- package/dist/modes/rpc/session-event-writer.d.ts.map +1 -1
- package/dist/modes/rpc/session-event-writer.js +21 -2
- package/dist/modes/rpc/session-event-writer.js.map +1 -1
- package/dist/modes/rpc/session-worker-client.d.ts +1 -0
- package/dist/modes/rpc/session-worker-client.d.ts.map +1 -1
- package/dist/modes/rpc/session-worker-client.js +3 -0
- package/dist/modes/rpc/session-worker-client.js.map +1 -1
- package/dist/modes/rpc/session-worker-protocol.d.ts +3 -1
- package/dist/modes/rpc/session-worker-protocol.d.ts.map +1 -1
- package/dist/modes/rpc/session-worker-protocol.js.map +1 -1
- package/dist/modes/rpc/session-worker.js +7 -1
- package/dist/modes/rpc/session-worker.js.map +1 -1
- package/docs/providers.md +9 -1
- package/docs/rpc.md +58 -8
- package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +15 -0
- package/node_modules/@code-yeongyu/senpi-codemode/README.md +51 -13
- package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
- package/node_modules/@code-yeongyu/senpi-codemode/src/bridges/agent-bridge.ts +53 -9
- package/node_modules/@code-yeongyu/senpi-codemode/src/bridges/reserved-dispatch.ts +1 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/config/settings.ts +18 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/extension/eval-status.ts +9 -4
- package/node_modules/@code-yeongyu/senpi-codemode/src/index.ts +3 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/AGENTS.md +2 -2
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/jl/prelude.jl +10 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/context-manager.ts +18 -6
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-contract.ts +2 -6
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/prelude.ts +1 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/run-queue.ts +17 -7
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-runtime.js +6 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/kernel-contract.ts +2 -5
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/kernel.ts +37 -5
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/prelude.py +11 -2
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/rb/prelude.rb +6 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-contract.ts +2 -5
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-kernel.ts +14 -4
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-queue.ts +18 -5
- package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt-template.ts +3 -2
- package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt.ts +4 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/timeouts/idle-timeout.ts +9 -2
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/AGENTS.md +2 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/cell-execution.ts +19 -2
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/cell-runtime.ts +11 -3
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-contract.ts +6 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-manager.ts +72 -20
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-snapshot.ts +28 -4
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-state.ts +5 -4
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-status.ts +5 -2
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-eval-result.ts +76 -25
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-execution-event.ts +4 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-kernel-reset-refused-error.ts +12 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-request.ts +2 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool-options.ts +3 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool.ts +58 -37
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/managed-cell.ts +15 -8
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/render.ts +20 -6
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/run-eval-cell.ts +89 -25
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/terminal-snapshot-store.ts +4 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/types.ts +51 -10
- package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
- package/node_modules/@earendil-works/pi-ai/README.md +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js +14 -56
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js +18 -27
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/websocket-transport-failure.d.ts +40 -0
- package/node_modules/@earendil-works/pi-ai/dist/api/websocket-transport-failure.d.ts.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/api/websocket-transport-failure.js +102 -0
- package/node_modules/@earendil-works/pi-ai/dist/api/websocket-transport-failure.js.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/kimi-coding.d.ts +4 -3
- package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/kimi-coding.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/kimi-coding.js +28 -14
- package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/kimi-coding.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/kimi-region.d.ts +44 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/kimi-region.d.ts.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/kimi-region.js +120 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/kimi-region.js.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/failover.d.ts +2 -2
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/failover.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/failover.js +2 -2
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/failover.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/slots.d.ts +3 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/slots.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/slots.js +21 -15
- package/node_modules/@earendil-works/pi-ai/dist/auth/pool/slots.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/index.d.ts +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/index.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/index.js +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/index.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-workers-ai.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/huggingface.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding-auth.d.ts +9 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding-auth.d.ts.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding-auth.js +43 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding-auth.js.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.js +3 -2
- package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/provider-failure-description.d.ts +19 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/provider-failure-description.d.ts.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/provider-failure-description.js +63 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/provider-failure-description.js.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.d.ts +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +2 -2
- package/node_modules/@earendil-works/pi-pty/package.json +1 -1
- package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- package/package.json +7 -7
- package/dist/bundle/chunks/chunk-5J5R5AMP.js +0 -8
- package/dist/bundle/chunks/chunk-I3F6VZZF.js +0 -2
- package/dist/bundle/chunks/host-lifecycle-DGZJTBDJ.js +0 -3
- package/dist/bundle/chunks/openai-codex-responses-L2PO2D6F.js +0 -10
- package/dist/bundle/chunks/openai-responses-YDUBVF4S.js +0 -2
- package/dist/bundle/chunks/rotation-stream-Y4YU6HV4.js +0 -2
package/docs/providers.md
CHANGED
|
@@ -22,6 +22,7 @@ Use `/login` in interactive mode, then select a provider:
|
|
|
22
22
|
- GitHub Copilot
|
|
23
23
|
- xAI (Grok/X subscription)
|
|
24
24
|
- OpenRouter (OAuth-minted API key billed from OpenRouter credits)
|
|
25
|
+
- Kimi Code (kimi.com / kimi.ai subscriptions)
|
|
25
26
|
- Radius
|
|
26
27
|
- Cursor (Pro/Ultra/Teams) — authentication only for now, see below
|
|
27
28
|
|
|
@@ -136,6 +137,13 @@ Upstreams that require explicit cache breakpoints get `cache_control` blocks thr
|
|
|
136
137
|
|
|
137
138
|
senpi sends `prompt_cache_key` (set to the session id) on Moonshot requests. Kimi documents the field as required for the Kimi Code Plan and recommended for any multi-turn agent ([Kimi context caching](https://platform.kimi.ai/docs/guide/use-context-caching-feature-of-kimi-api)). Kimi reports cache hits as a flat `usage.cached_tokens` field, which senpi parses as cache-read tokens.
|
|
138
139
|
|
|
140
|
+
### Kimi Code
|
|
141
|
+
|
|
142
|
+
- Run `/login kimi-coding`, then select **Sign in with Kimi Code** and pick the service that hosts your account: **Mainland China (kimi.com)** or **Outside mainland China (kimi.ai)**
|
|
143
|
+
- The region is stored with the credential; token refresh and model requests follow it (`auth.kimi.ai` + `api.kimi.ai/coding` for international accounts), so a `.ai` login keeps working after restarts and env changes
|
|
144
|
+
- **Use an API key** asks the same region question and routes `KIMI_API_KEY` requests by it; `KIMI_CODE_REGION=global` (or `mainland-cn`) answers it for headless setups
|
|
145
|
+
- `KIMI_CODE_OAUTH_HOST` / `KIMI_OAUTH_HOST` still override the auth host for new logins and for credentials saved before regions existed; a host other than the two official ones is kept verbatim and needs a `models.json` `baseUrl` for inference
|
|
146
|
+
|
|
139
147
|
### Radius
|
|
140
148
|
|
|
141
149
|
Radius is a dynamic `pi-messages` gateway. `/login radius` stores OAuth tokens in `auth.json`; the gateway catalog is refreshed independently and cached in `models-store.json`. Custom Radius gateways can be declared in `models.json` with `"oauth": "radius"` and a gateway `baseUrl`.
|
|
@@ -286,7 +294,7 @@ senpi
|
|
|
286
294
|
| Fireworks | `FIREWORKS_API_KEY` | `fireworks` |
|
|
287
295
|
| Together AI | `TOGETHER_API_KEY` | `together` |
|
|
288
296
|
| Baseten | `BASETEN_API_KEY` | `baseten` |
|
|
289
|
-
| Kimi For Coding | `KIMI_API_KEY` | `kimi-coding` |
|
|
297
|
+
| Kimi For Coding | `KIMI_API_KEY` (+ `KIMI_CODE_REGION`) | `kimi-coding` |
|
|
290
298
|
| MiniMax | `MINIMAX_API_KEY` | `minimax` |
|
|
291
299
|
| MiniMax (China) | `MINIMAX_CN_API_KEY` | `minimax-cn` |
|
|
292
300
|
| Qwen Token Plan (existing catalog) | `QWEN_TOKEN_PLAN_API_KEY` | `qwen-token-plan` |
|
package/docs/rpc.md
CHANGED
|
@@ -207,7 +207,13 @@ are what this decision encodes; the other two are what a client must not undo el
|
|
|
207
207
|
`ensureHost` records a `writer: { pid, startTime }` stamp in its pidfile and stops the host it names only when
|
|
208
208
|
that stamp is this process (the start time is what keeps a recycled pid from inheriting the right). A pidfile
|
|
209
209
|
written by anyone else fails the ensure with `HostEnsureRefusedError { reason: "foreign_writer" }` and the host
|
|
210
|
-
keeps running.
|
|
210
|
+
keeps running. That refusal is about signalling, so it holds only while something still accepts connections at
|
|
211
|
+
the public path: a registration that names a live process whose entry is GONE (it was replaced by another socket
|
|
212
|
+
and the replacement later exited) or whose entry nobody listens behind (a dead listener left it, a successor's
|
|
213
|
+
rename never landed) describes a generation nobody can reach, and binding a fresh generation to the free path
|
|
214
|
+
touches nothing of it. The ensure then starts one beside it, numbered after the stranded generation, which
|
|
215
|
+
keeps its registration until it exits; refusing there locked every client out until that process happened to
|
|
216
|
+
end (#1936). A named pipe has no entry to lose, so win32 keeps refusing.
|
|
211
217
|
- **I2 - compatibility is protocol version + capabilities, never semver equality.** An ordinal that cannot be
|
|
212
218
|
compared (a build without git metadata, a host that reports none) is EQUAL, and since a handoff requires
|
|
213
219
|
STRICTLY greater, such a pair attaches instead of upgrading.
|
|
@@ -238,10 +244,11 @@ client's sessions. A GENERATION HANDOFF replaces the process while its sessions
|
|
|
238
244
|
running host keeps serving.
|
|
239
245
|
3. `handoffHost` then registers the successor (its own `generations/<instanceId>/host.pid`, the pointer
|
|
240
246
|
moved onto it, and `settings.json { socket, generation }`) and sends
|
|
241
|
-
the predecessor SIGUSR1 = DRAIN: stop accepting,
|
|
242
|
-
|
|
243
|
-
see `session_closed { reason: "handoff_parked" }`
|
|
244
|
-
`open_session { sessionPath }
|
|
247
|
+
the predecessor SIGUSR1 = DRAIN: stop accepting, announce `host_superseded` to every connection,
|
|
248
|
+
park attached and unattached sessions as their turns and requests settle, then exit independently of
|
|
249
|
+
connection occupancy. Clients see `session_closed { reason: "handoff_parked", sessionPath }` before
|
|
250
|
+
their connection closes and reopen with `open_session { sessionPath }` on the successor. Losing
|
|
251
|
+
ownership of the public socket triggers the same sequence even if SIGUSR1 never arrives.
|
|
245
252
|
|
|
246
253
|
Two guards decide whether a handoff is attempted at all, and both fail closed:
|
|
247
254
|
|
|
@@ -485,7 +492,7 @@ exposing session content. The one exception is `session_closed` and `session_par
|
|
|
485
492
|
`kind: "worker"`: they are delivered only to
|
|
486
493
|
the connections attached to that session, so machine-driven work neither appears in nor disappears from a client that never
|
|
487
494
|
asked for it. Every other lifecycle record, and every record of an `interactive` session, keeps today's broadcast. Host-level
|
|
488
|
-
records (`host_stalled`, `host_memory_pressure`) are broadcast the same way and describe the host process rather than a
|
|
495
|
+
records (`host_superseded`, `host_stalled`, `host_memory_pressure`) are broadcast the same way and describe the host process rather than a
|
|
489
496
|
session. Correlated responses and dialog extension UI
|
|
490
497
|
requests (select, confirm, input, and editor) are requester-only; other extension UI state records go to the session's
|
|
491
498
|
attached connections. To observe a foreign session, open it by its existing
|
|
@@ -599,6 +606,48 @@ internal directory. The supervisor also exports `SENPI_RPC_HOST_WATCH_PPID` as a
|
|
|
599
606
|
set only by the supervisor: a host started any other way (plain `senpi --mode rpc --listen …`, embedders, hand-started
|
|
600
607
|
hosts) sees neither variable and is unaffected. A host whose supervisor is alive is never touched by this binding.
|
|
601
608
|
|
|
609
|
+
### Supersession lifecycle: `host_superseded`
|
|
610
|
+
|
|
611
|
+
Before parking for a generation handoff, the old host sends every connected client one unsolicited record:
|
|
612
|
+
|
|
613
|
+
```json
|
|
614
|
+
{"type":"host_superseded","instanceId":"old-host-uuid","generation":0,"successor":{"socket":"/tmp/agent/rpc.sock"}}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
`instanceId` and `generation` identify the OLD host, matching its `get_protocol_info` response. `successor.socket`
|
|
618
|
+
identifies the public socket, never the supervisor's private hop. `successor` is `null` when a drain is requested
|
|
619
|
+
without a known replacement. This record has no request id or session handle; older clients can ignore it.
|
|
620
|
+
|
|
621
|
+
1. The announcement is queued on every connection before handoff parking changes any session.
|
|
622
|
+
2. Every parkable session, attached or unattached, is parked immediately. A session is **parkable** when no agent
|
|
623
|
+
run, bash/tool execution, compaction, queued session work, or in-flight host request remains. Accepted opens,
|
|
624
|
+
binding creation, close requests, and steer delivery must finish. This is the ordinary busy predicate with
|
|
625
|
+
**durable wake-source holds excluded**: persistent monitors, scheduled continuations and goal-loop holds do not
|
|
626
|
+
pin an old generation. They resume from the persisted session when reopened, as after a restart. Ordinary idle
|
|
627
|
+
eviction retains its broader activity predicate, including all wake sources.
|
|
628
|
+
3. Active sessions keep serving their existing connections and park when their turns and requests settle. Each
|
|
629
|
+
released handle emits `session_closed { sessionId, reason: "handoff_parked", sessionPath }` after releasing its
|
|
630
|
+
file. This is not a session deletion: reopen `sessionPath` on the successor to obtain a new routing handle.
|
|
631
|
+
That `sessionPath` is the CANONICAL path the host reserved the file under, which is not necessarily the string
|
|
632
|
+
the client passed to `open_session` (a scratch dir under `/var/...` on macOS is reported as `/private/var/...`).
|
|
633
|
+
A client matching the record against its own stored path must canonicalize before comparing; reopening with
|
|
634
|
+
either spelling resolves to the same reservation.
|
|
635
|
+
The connection closes after its last attached session parks; a connection shared with another active session
|
|
636
|
+
stays open until that session also settles. A command racing parking on the old connection is either served
|
|
637
|
+
or answered by that terminal record and close, never `unknown_session` for the parked handle. New opens on a
|
|
638
|
+
draining connection are refused with `host_draining`.
|
|
639
|
+
4. The **supervisor** starts `SENPI_RPC_HANDOFF_GRACE_MS` at supersession (default **600000 ms / 10 minutes**).
|
|
640
|
+
This is a soft grace, not an abort deadline or a delay before parking. At expiry it reports outstanding work
|
|
641
|
+
and requests another drain pass. Sessions still mid-turn or handling requests remain alive even beyond grace;
|
|
642
|
+
no turn is cut short. Parkable sessions never wait for grace. A bare socket host has no supervisor grace timer,
|
|
643
|
+
but follows the same announce/park/settle sequence.
|
|
644
|
+
5. Once every session has parked and its terminal records have drained, the generation exits even if an observer
|
|
645
|
+
or client never voluntarily disconnects. Neither the normal idle timeout nor persistent-host mode can pin it.
|
|
646
|
+
|
|
647
|
+
A client may react to `host_superseded` early, but the old generation retains each file until its work settles;
|
|
648
|
+
`session_path_in_use` on the successor remains a retryable response until parking releases that claim. A client
|
|
649
|
+
that ignores the announcement still receives the terminal record and connection close.
|
|
650
|
+
|
|
602
651
|
### Shared host occupancy (idle eviction, retention, empty-host exit)
|
|
603
652
|
|
|
604
653
|
**The daemon does not cap sessions.** On the in-process runtime - the default for a `--listen` socket host, i.e. the
|
|
@@ -619,8 +668,8 @@ What the host does enforce are lifecycle windows, and they only ever return memo
|
|
|
619
668
|
`session_parked { sessionId, sessionPath }` and there is no `close_session` response, because nothing closed the session -
|
|
620
669
|
the routing handle was released while the session itself stays on disk and reopens with `open_session { sessionPath }`
|
|
621
670
|
(as a NEW handle). A client that does not know `session_parked` ignores it and learns the handle is gone from its next
|
|
622
|
-
command's `unknown_session`. A GENERATION HANDOFF parks
|
|
623
|
-
|
|
671
|
+
command's `unknown_session`. A GENERATION HANDOFF also parks attached sessions, using the distinct activity
|
|
672
|
+
predicate below, and emits `session_closed { sessionId, reason: "handoff_parked", sessionPath }`, so a client can tell "the host handed
|
|
624
673
|
over, reopen by path" from "this session ended". An explicit `close_session` is `reason: "client_close"`; the host
|
|
625
674
|
process exiting is `reason: "host_shutdown"` (even for a retained session - the process is going away, so this is not
|
|
626
675
|
a park). `reason` is optional on the wire; a client that does not know a
|
|
@@ -836,6 +885,7 @@ In the response `error` field, machine-matchable:
|
|
|
836
885
|
- `session_reservation_limit` (this worker already holds 64 live session paths; the open or session replacement was refused without disturbing the existing session)
|
|
837
886
|
- `missing_session_id` (session-scoped command without `sessionId` in multi mode)
|
|
838
887
|
- `multi_session_disabled` (`open_session` in classic mode)
|
|
888
|
+
- `host_draining` (`open_session` on a connection whose generation is parking for a handoff; the successor already owns the public path, so re-resolve it and open there rather than retrying this connection)
|
|
839
889
|
- `invalid_path` (relative `sessionPath`/`cwd`)
|
|
840
890
|
- `open_failed: <detail>`
|
|
841
891
|
- `invalid_session_context: <detail>` (`open_session.context` past a documented cap: more than 32 keys, a key that does not match `^[a-z][a-z0-9_]*$`, a non-string or >16 KiB value, or more than 32 KiB of JSON in total; the detail names the cap and its byte budget)
|
|
@@ -12,6 +12,21 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.9.22] - 2026-09-21
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `eval` runs up to `maxDetachedCells` background cells per session (default 15, `SENPI_CODEMODE_MAX_DETACHED_CELLS`) and queues same-language cells on their kernel instead of rejecting them; `list` observes live and recent cells, and resetting a busy language refuses with `eval_kernel_busy_reset_refused` instead of stopping live work. ([#1908](https://github.com/code-yeongyu/senpi/issues/1908))
|
|
22
|
+
- `agent()` forwards `isolated`, `apply`, and `merge` when the task host advertises isolation. Hosts that do not still drop those options with the existing warning. A foreground call whose isolation did not apply now raises instead of looking successful; with `handle: true` the isolation result arrives on completion. ([#1910](https://github.com/code-yeongyu/senpi/issues/1910))
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
### Removed
|
|
29
|
+
|
|
15
30
|
## [2026.9.21-2] - 2026-09-21
|
|
16
31
|
|
|
17
32
|
### Breaking Changes
|
|
@@ -93,6 +93,7 @@ Configuration is loaded in this order:
|
|
|
93
93
|
"foregroundWindowSeconds": 60,
|
|
94
94
|
"runBudgetSeconds": 300,
|
|
95
95
|
"hardLimitSeconds": 1800,
|
|
96
|
+
"maxDetachedCells": 15,
|
|
96
97
|
"parallelPoolWidth": 4,
|
|
97
98
|
"taskTools": {
|
|
98
99
|
"task": "task",
|
|
@@ -110,9 +111,10 @@ Configuration is loaded in this order:
|
|
|
110
111
|
| --- | --- | --- |
|
|
111
112
|
| `languages` | `py`/`js` enabled; `rb`/`jl` disabled | Selects desired languages before interpreter detection. |
|
|
112
113
|
| `cellTimeoutSeconds` | `30` | Idle time an interactive call blocks the turn before the cell detaches. Print/json calls never detach. |
|
|
113
|
-
| `foregroundWindowSeconds` | `60` |
|
|
114
|
-
| `runBudgetSeconds` | `300` | Kill deadline for a cell's own execution time - child processes, network, timers, CPU. Time parked in host tool calls (`agent()`, `tool.*`) is not charged, and the budget keeps counting after the cell detaches. A per-call `timeout` replaces it for that cell. Env override: `SENPI_CODEMODE_RUN_BUDGET_SECONDS`. |
|
|
115
|
-
| `hardLimitSeconds` | `1800` | Wall-clock kill deadline
|
|
114
|
+
| `foregroundWindowSeconds` | `60` | Submission-based foreground limit, including queue and bridge pauses. The cell detaches if capacity is available; otherwise it is cancelled with `eval_background_capacity_reached`. Env override: `SENPI_CODEMODE_FOREGROUND_SECONDS`. |
|
|
115
|
+
| `runBudgetSeconds` | `300` | Kill deadline for a cell's own execution time - child processes, network, timers, CPU. Time queued or parked in host tool calls (`agent()`, `tool.*`) is not charged, and the budget keeps counting after the cell detaches. A per-call `timeout` replaces it for that cell. Env override: `SENPI_CODEMODE_RUN_BUDGET_SECONDS`. |
|
|
116
|
+
| `hardLimitSeconds` | `1800` | Wall-clock kill deadline from submission, including queue and parked time; a per-call `timeout` above it raises it. Env override: `SENPI_CODEMODE_HARD_LIMIT_SECONDS`. |
|
|
117
|
+
| `maxDetachedCells` | `15` | Global detached-cell capacity across all kernels, including queued cells. At capacity, interactive cells stay foreground until completion or the foreground window elapses. Env override: `SENPI_CODEMODE_MAX_DETACHED_CELLS`. |
|
|
116
118
|
| `parallelPoolWidth` | `4` | Maximum concurrent `parallel()` thunks. |
|
|
117
119
|
| `taskTools.task` | `"task"` | Registered tool name used by `agent()`. |
|
|
118
120
|
| `taskTools.output` | `"task_output"` | Registered tool name used by `output()`. |
|
|
@@ -126,6 +128,9 @@ enables; `0` or `false` disables. Any other value leaves the file setting in
|
|
|
126
128
|
effect.
|
|
127
129
|
|
|
128
130
|
Malformed JSON or invalid settings fall back to defaults with a warning.
|
|
131
|
+
The detached-cell environment override uses the run-budget parser: a positive
|
|
132
|
+
base-10 integer wins over the file value; zero, negative, and malformed values
|
|
133
|
+
leave the file value in effect.
|
|
129
134
|
|
|
130
135
|
## Cell helpers
|
|
131
136
|
|
|
@@ -162,13 +167,25 @@ tasks return the requested transcript. Missing tools produce a clear
|
|
|
162
167
|
availability error instead of importing an orchestration package. `agent()`
|
|
163
168
|
delegates through the tool contract, so task-engine permissions, progress
|
|
164
169
|
updates, and transcripts remain owned by that engine.
|
|
165
|
-
`isolated
|
|
166
|
-
|
|
170
|
+
`isolated` and `apply` accept booleans; `merge` accepts `"patch"` or `"branch"`
|
|
171
|
+
(and `false`/`true` aliases respectively). The bridge checks the configured task
|
|
172
|
+
parameter schema once per bridge, using the same host catalog as `tool_schema()`.
|
|
173
|
+
If it advertises `isolated`, these options are forwarded; otherwise they are
|
|
174
|
+
omitted with the existing warning. Senpi does not implement isolation itself.
|
|
175
|
+
A foreground host result with `details.isolation.changes_applied === false`
|
|
176
|
+
raises `AgentIsolationNotAppliedError` (`isolation_not_applied`), including any
|
|
177
|
+
`patch_path`, `branch_name`, and `manual_command` recovery fields in its message.
|
|
178
|
+
The bridge retains host `details.isolation`; successful foreground helpers still
|
|
179
|
+
return text or parsed JSON.
|
|
167
180
|
|
|
168
181
|
Background `agent()` handles retain `id` and `agent://<id>` and include `run_epoch`.
|
|
169
182
|
The host must return structured `details.task_id` (`st_` plus lowercase hex) and
|
|
170
183
|
an integer `details.run_epoch >= 0`. Missing or malformed details raise
|
|
171
|
-
`invalid_task_handle`; prose IDs are never used.
|
|
184
|
+
`invalid_task_handle`; prose IDs are never used. Handles return immediately,
|
|
185
|
+
so final isolation results are not available on the initial handle: await the
|
|
186
|
+
completion notification or read `task_output` (via `output()`) after completion.
|
|
187
|
+
Any isolation metadata supplied by the host on a handle is preserved as
|
|
188
|
+
`details.isolation`, not interpreted as a foreground apply failure.
|
|
172
189
|
|
|
173
190
|
`workpool` takes exactly one of `{category, prompt, model?}` or
|
|
174
191
|
`{subagent_type, prompt, model?}` as its plain-data agent spec. Mode is `fresh`
|
|
@@ -203,22 +220,28 @@ without a `summary` fails with a teaching error.
|
|
|
203
220
|
`eval` accepts `on_timeout: "detach"|"error"`. The default is `"detach"` in
|
|
204
221
|
interactive TUI, RPC, and app-server sessions; print and JSON one-shot runs
|
|
205
222
|
default to `"error"` so their result is never silently detached. A detached
|
|
206
|
-
cell keeps only its own language kernel busy.
|
|
207
|
-
|
|
208
|
-
|
|
223
|
+
cell keeps only its own language kernel busy. New same-language cells are admitted
|
|
224
|
+
into its FIFO queue; calls in other languages continue normally. Queued cells may
|
|
225
|
+
also detach, within the global `maxDetachedCells` cap. At capacity, the first idle
|
|
226
|
+
detach attempt leaves the cell foreground and re-arms one wait for the remaining
|
|
227
|
+
submission-based foreground window. At the window, it detaches if a slot is now
|
|
228
|
+
free; otherwise it settles `cancelled` with `eval_background_capacity_reached`,
|
|
229
|
+
listing the live cells and a stop-or-wait remedy. Cells that complete inside the
|
|
230
|
+
window return normally. Cancelling a queued cell never interrupts its predecessor.
|
|
231
|
+
Do not re-run a detached or queued cell; each detached cell completes as one notification.
|
|
209
232
|
|
|
210
233
|
Queued steering also detaches an eligible interactive foreground call, including
|
|
211
234
|
one paused in a host tool bridge, without cancelling its computation. If the
|
|
212
|
-
|
|
235
|
+
global detached cap is reached, steering leaves the call waiting. Follow-up
|
|
213
236
|
messages, explicit `on_timeout: "error"`, and print/JSON calls do not trigger this
|
|
214
237
|
transition; caller abort and existing deadlines retain their cancellation behavior.
|
|
215
238
|
|
|
216
239
|
Every cell, detached or not, is bounded by two kill deadlines. The run budget
|
|
217
240
|
(`runBudgetSeconds`, or the call's `timeout`) charges only the cell's own
|
|
218
|
-
execution time and is paused while a host tool call is in flight, so a cell
|
|
241
|
+
execution time and is paused while queued or while a host tool call is in flight, so a cell
|
|
219
242
|
waiting on `agent()` survives while a runaway child process or loop does not.
|
|
220
243
|
The hard limit (`hardLimitSeconds`, raised by a larger `timeout`) is wall-clock
|
|
221
|
-
and bounds parked cells too. A cell killed by either deadline reports which one
|
|
244
|
+
from submission and bounds queued and parked cells too. A cell killed by either deadline reports which one
|
|
222
245
|
in its result or completion notification, together with whether kernel state
|
|
223
246
|
survived; the tool schema states the configured numbers. The `timeout` value
|
|
224
247
|
never changes the idle detach deadline: that is `cellTimeoutSeconds` capped by
|
|
@@ -227,9 +250,24 @@ never changes the idle detach deadline: that is `cellTimeoutSeconds` capped by
|
|
|
227
250
|
While any cell is detached, the interactive footer shows a highlighted
|
|
228
251
|
`↗ <language> · <summary>` status on the extension status line (the cell id
|
|
229
252
|
when the call had no summary), clearing as soon as the last detached cell settles.
|
|
253
|
+
Queued entries are labelled `queued`; an all-queued footer shows `(queued)` instead
|
|
254
|
+
of an elapsed duration. Elapsed time counts only from execution start.
|
|
255
|
+
|
|
256
|
+
Use `eval({ action: "list" })` without a language or cell id to see live and
|
|
257
|
+
recently settled cells across languages. Each line includes the id, language,
|
|
258
|
+
state, elapsed execution seconds, queue predecessors, and summary (or a short code
|
|
259
|
+
preview); `details.cells` carries the typed cell metadata. Listing never consumes
|
|
260
|
+
completion notifications.
|
|
261
|
+
|
|
262
|
+
A run with `reset: true` resets only its selected language and is refused with
|
|
263
|
+
`eval_kernel_busy_reset_refused` while any other cell in that language is live,
|
|
264
|
+
including queued cells. The requesting cell fails without changing the kernel or
|
|
265
|
+
cancelling existing work. Stop those cells explicitly or wait for their completion
|
|
266
|
+
notifications before resetting; live cells in other languages do not block reset.
|
|
230
267
|
|
|
231
268
|
Use `eval({ action: "peek", cell_id })` for its state and buffered output, or
|
|
232
|
-
`eval({ action: "stop", cell_id })` to cancel it.
|
|
269
|
+
`eval({ action: "stop", cell_id })` to cancel it. Stopping a queued cell removes it
|
|
270
|
+
without interrupting the active cell; kernel state is retained. Python running-cell stop interrupts the
|
|
233
271
|
existing kernel and preserves variables. JavaScript stop is cooperative first:
|
|
234
272
|
the worker rejects the cell's pending bridge `tool.*` calls and kills the
|
|
235
273
|
`Bun.spawn` children it started, and a cell that settles within the 2 s grace
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.22",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Source-only senpi extension package for codemode evaluation tools",
|
|
6
6
|
"type": "module",
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@babel/parser": "8.0.4",
|
|
34
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.
|
|
34
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.22",
|
|
35
35
|
"typebox": "1.3.34"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
38
|
+
"@code-yeongyu/senpi": "2026.9.22"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
41
|
+
"@code-yeongyu/senpi": "2026.9.22"
|
|
42
42
|
},
|
|
43
43
|
"keywords": [
|
|
44
44
|
"senpi",
|
|
@@ -2,6 +2,7 @@ import type { AgentToolResult } from "@code-yeongyu/senpi";
|
|
|
2
2
|
import { type Static, Type } from "typebox";
|
|
3
3
|
import { Check, Errors } from "typebox/value";
|
|
4
4
|
import type { EvalStatusEvent, ExecuteTool } from "../tool/types.ts";
|
|
5
|
+
import type { EvalSchemaToolInfo } from "./schema-bridge.ts";
|
|
5
6
|
|
|
6
7
|
const agentArgsSchema = Type.Object(
|
|
7
8
|
{
|
|
@@ -14,7 +15,7 @@ const agentArgsSchema = Type.Object(
|
|
|
14
15
|
tools: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
15
16
|
isolated: Type.Optional(Type.Boolean()),
|
|
16
17
|
apply: Type.Optional(Type.Boolean()),
|
|
17
|
-
merge: Type.Optional(Type.Boolean()),
|
|
18
|
+
merge: Type.Optional(Type.Union([Type.Boolean(), Type.Literal("patch"), Type.Literal("branch")])),
|
|
18
19
|
},
|
|
19
20
|
{ additionalProperties: false },
|
|
20
21
|
);
|
|
@@ -43,6 +44,9 @@ type TaskParams = {
|
|
|
43
44
|
readonly name?: string;
|
|
44
45
|
readonly run_in_background: boolean;
|
|
45
46
|
readonly tools?: readonly string[];
|
|
47
|
+
readonly isolated?: boolean;
|
|
48
|
+
readonly apply?: boolean;
|
|
49
|
+
readonly merge?: "patch" | "branch";
|
|
46
50
|
};
|
|
47
51
|
type ProgressContext = { readonly fallbackId: string; readonly warning?: string };
|
|
48
52
|
|
|
@@ -54,15 +58,32 @@ export interface RunEvalAgentOptions {
|
|
|
54
58
|
readonly callId: string;
|
|
55
59
|
readonly taskToolName: string;
|
|
56
60
|
readonly executeTool: AgentExecuteTool;
|
|
61
|
+
readonly listTools?: () => readonly EvalSchemaToolInfo[];
|
|
57
62
|
readonly signal?: AbortSignal;
|
|
58
63
|
readonly emitStatus?: (event: EvalStatusEvent) => void;
|
|
59
64
|
}
|
|
60
65
|
|
|
61
|
-
export type EvalAgentResult =
|
|
66
|
+
export type EvalAgentResult = (
|
|
62
67
|
| { readonly text: string }
|
|
63
68
|
| { readonly text: string; readonly data: unknown }
|
|
64
69
|
| { readonly text: string; readonly parseError: string }
|
|
65
|
-
| { readonly text: string; readonly id: string; readonly handle: string; readonly run_epoch: number }
|
|
70
|
+
| { readonly text: string; readonly id: string; readonly handle: string; readonly run_epoch: number }
|
|
71
|
+
) & { readonly details?: { readonly isolation: Readonly<Record<string, unknown>> } };
|
|
72
|
+
|
|
73
|
+
// The session-owned executor identifies a bridge across JS and HTTP calls; do not retain retired sessions.
|
|
74
|
+
const isolationCapabilities = new WeakMap<AgentExecuteTool, Map<string, boolean>>();
|
|
75
|
+
|
|
76
|
+
export class AgentIsolationNotAppliedError extends Error {
|
|
77
|
+
readonly name = "AgentIsolationNotAppliedError";
|
|
78
|
+
readonly code = "isolation_not_applied";
|
|
79
|
+
|
|
80
|
+
constructor(isolation: Readonly<Record<string, unknown>>) {
|
|
81
|
+
const recovery = ["patch_path", "branch_name", "manual_command"]
|
|
82
|
+
.flatMap((key) => (typeof isolation[key] === "string" ? [`${key}: ${isolation[key]}`] : []))
|
|
83
|
+
.join("; ");
|
|
84
|
+
super(`agent() isolated changes were not applied${recovery ? `; ${recovery}` : ""}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
66
87
|
|
|
67
88
|
class AgentArgumentsError extends Error {
|
|
68
89
|
readonly name = "AgentArgumentsError";
|
|
@@ -92,7 +113,8 @@ class AgentHandleError extends Error {
|
|
|
92
113
|
export async function runEvalAgent(args: unknown, options: RunEvalAgentOptions): Promise<EvalAgentResult> {
|
|
93
114
|
const parsed = parseAgentArgs(args);
|
|
94
115
|
const structured = Object.hasOwn(parsed, "schema");
|
|
95
|
-
const
|
|
116
|
+
const supportsIsolation = taskSupportsIsolation(options);
|
|
117
|
+
const warning = supportsIsolation ? undefined : droppedOptionsWarning(parsed);
|
|
96
118
|
const fallbackId = parsed.label ?? options.callId;
|
|
97
119
|
if (warning) options.emitStatus?.({ op: "agent", id: fallbackId, status: "running", warning });
|
|
98
120
|
|
|
@@ -104,7 +126,7 @@ export async function runEvalAgent(args: unknown, options: RunEvalAgentOptions):
|
|
|
104
126
|
|
|
105
127
|
let result: AgentToolResult<unknown>;
|
|
106
128
|
try {
|
|
107
|
-
result = await options.executeTool(options.taskToolName, toTaskParams(parsed, structured), {
|
|
129
|
+
result = await options.executeTool(options.taskToolName, toTaskParams(parsed, structured, supportsIsolation), {
|
|
108
130
|
...(options.signal ? { signal: options.signal } : {}),
|
|
109
131
|
...(options.emitStatus
|
|
110
132
|
? {
|
|
@@ -119,12 +141,15 @@ export async function runEvalAgent(args: unknown, options: RunEvalAgentOptions):
|
|
|
119
141
|
}
|
|
120
142
|
|
|
121
143
|
const text = resultText(result);
|
|
144
|
+
const isolation =
|
|
145
|
+
isRecord(result.details) && isRecord(result.details.isolation) ? result.details.isolation : undefined;
|
|
146
|
+
const metadata = isolation === undefined ? {} : { details: { isolation } };
|
|
122
147
|
if (parsed.handle === true) {
|
|
123
148
|
const { task_id: id, run_epoch } = resultTaskHandle(result);
|
|
124
|
-
return { text, id, handle: `agent://${id}`, run_epoch };
|
|
149
|
+
return { text, id, handle: `agent://${id}`, run_epoch, ...metadata };
|
|
125
150
|
}
|
|
126
|
-
if (
|
|
127
|
-
return parseStructuredText(text);
|
|
151
|
+
if (isolation?.changes_applied === false) throw new AgentIsolationNotAppliedError(isolation);
|
|
152
|
+
return { ...(structured ? parseStructuredText(text) : { text }), ...metadata };
|
|
128
153
|
}
|
|
129
154
|
|
|
130
155
|
function parseAgentArgs(value: unknown): AgentArgs {
|
|
@@ -135,7 +160,21 @@ function parseAgentArgs(value: unknown): AgentArgs {
|
|
|
135
160
|
throw new AgentArgumentsError(summary || "invalid value");
|
|
136
161
|
}
|
|
137
162
|
|
|
138
|
-
function
|
|
163
|
+
function taskSupportsIsolation(options: RunEvalAgentOptions): boolean {
|
|
164
|
+
let capabilities = isolationCapabilities.get(options.executeTool);
|
|
165
|
+
if (!capabilities) {
|
|
166
|
+
capabilities = new Map();
|
|
167
|
+
isolationCapabilities.set(options.executeTool, capabilities);
|
|
168
|
+
}
|
|
169
|
+
const cached = capabilities.get(options.taskToolName);
|
|
170
|
+
if (cached !== undefined) return cached;
|
|
171
|
+
const schema = options.listTools?.().find((tool) => tool.name === options.taskToolName)?.parameters;
|
|
172
|
+
const supported = isRecord(schema) && isRecord(schema.properties) && schema.properties.isolated !== undefined;
|
|
173
|
+
capabilities.set(options.taskToolName, supported);
|
|
174
|
+
return supported;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function toTaskParams(args: AgentArgs, structured: boolean, supportsIsolation: boolean): TaskParams {
|
|
139
178
|
return {
|
|
140
179
|
prompt: structured
|
|
141
180
|
? `${args.prompt}\n\nRespond ONLY with JSON matching this JSON-Schema:\n${JSON.stringify(args.schema)}`
|
|
@@ -145,6 +184,11 @@ function toTaskParams(args: AgentArgs, structured: boolean): TaskParams {
|
|
|
145
184
|
...(args.label === undefined ? {} : { name: args.label }),
|
|
146
185
|
run_in_background: args.handle === true,
|
|
147
186
|
...(args.tools === undefined ? {} : { tools: args.tools }),
|
|
187
|
+
...(supportsIsolation && args.isolated !== undefined ? { isolated: args.isolated } : {}),
|
|
188
|
+
...(supportsIsolation && args.apply !== undefined ? { apply: args.apply } : {}),
|
|
189
|
+
...(supportsIsolation && args.merge !== undefined
|
|
190
|
+
? { merge: args.merge === true ? "branch" : args.merge === false ? "patch" : args.merge }
|
|
191
|
+
: {}),
|
|
148
192
|
};
|
|
149
193
|
}
|
|
150
194
|
|
|
@@ -35,6 +35,7 @@ export async function runReservedTool(toolName: string, context: ReservedDispatc
|
|
|
35
35
|
callId: context.callId,
|
|
36
36
|
taskToolName: context.taskToolName,
|
|
37
37
|
executeTool: context.executeTool,
|
|
38
|
+
...(context.listTools === undefined ? {} : { listTools: context.listTools }),
|
|
38
39
|
...(context.signal === undefined ? {} : { signal: context.signal }),
|
|
39
40
|
emitStatus: context.emitStatus,
|
|
40
41
|
});
|
|
@@ -22,6 +22,7 @@ export const codemodeSettingsSchema = Type.Object(
|
|
|
22
22
|
foregroundWindowSeconds: Type.Optional(Type.Number({ minimum: 1 })),
|
|
23
23
|
runBudgetSeconds: Type.Optional(Type.Number({ minimum: 1 })),
|
|
24
24
|
hardLimitSeconds: Type.Optional(Type.Number({ minimum: 1 })),
|
|
25
|
+
maxDetachedCells: Type.Optional(Type.Number({ minimum: 1 })),
|
|
25
26
|
parallelPoolWidth: Type.Optional(Type.Number({ minimum: 1 })),
|
|
26
27
|
taskTools: Type.Optional(
|
|
27
28
|
Type.Object(
|
|
@@ -80,6 +81,8 @@ export interface CodemodeSettings {
|
|
|
80
81
|
readonly runBudgetSeconds: number;
|
|
81
82
|
/** Wall-clock kill deadline for a single cell; bounds detached and bridge-parked cells too. */
|
|
82
83
|
readonly hardLimitSeconds: number;
|
|
84
|
+
/** Maximum detached cells across all language kernels. */
|
|
85
|
+
readonly maxDetachedCells?: number;
|
|
83
86
|
readonly parallelPoolWidth: number;
|
|
84
87
|
readonly taskTools?: CodemodeTaskTools;
|
|
85
88
|
readonly outputSink?: CodemodeOutputSink;
|
|
@@ -87,6 +90,7 @@ export interface CodemodeSettings {
|
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
export type ResolvedCodemodeSettings = CodemodeSettings & {
|
|
93
|
+
readonly maxDetachedCells: number;
|
|
90
94
|
readonly taskTools: CodemodeTaskTools;
|
|
91
95
|
readonly outputSink: CodemodeOutputSink;
|
|
92
96
|
readonly statusEvents: boolean;
|
|
@@ -129,7 +133,10 @@ export const FOREGROUND_WINDOW_ENVIRONMENT_FLAG = "SENPI_CODEMODE_FOREGROUND_SEC
|
|
|
129
133
|
*/
|
|
130
134
|
export const DEFAULT_RUN_BUDGET_SECONDS = 300;
|
|
131
135
|
|
|
136
|
+
export const DEFAULT_MAX_DETACHED_CELLS = 15;
|
|
137
|
+
|
|
132
138
|
export const RUN_BUDGET_ENVIRONMENT_FLAG = "SENPI_CODEMODE_RUN_BUDGET_SECONDS";
|
|
139
|
+
export const MAX_DETACHED_CELLS_ENVIRONMENT_FLAG = "SENPI_CODEMODE_MAX_DETACHED_CELLS";
|
|
133
140
|
|
|
134
141
|
// OMP settings-schema.ts:3211-3299 has language/path settings only; eval.ts:427
|
|
135
142
|
// defaults timeout to 30s, and codemode pins concurrency-bridge.ts:30 width to 4.
|
|
@@ -144,6 +151,7 @@ export const defaultCodemodeSettings: ResolvedCodemodeSettings = {
|
|
|
144
151
|
foregroundWindowSeconds: DEFAULT_FOREGROUND_WINDOW_SECONDS,
|
|
145
152
|
runBudgetSeconds: DEFAULT_RUN_BUDGET_SECONDS,
|
|
146
153
|
hardLimitSeconds: DEFAULT_HARD_LIMIT_SECONDS,
|
|
154
|
+
maxDetachedCells: DEFAULT_MAX_DETACHED_CELLS,
|
|
147
155
|
parallelPoolWidth: 4,
|
|
148
156
|
taskTools: {
|
|
149
157
|
task: "task",
|
|
@@ -207,6 +215,15 @@ export function resolveRunBudgetSeconds(settings: CodemodeSettings, env: Environ
|
|
|
207
215
|
return positiveSecondsOverride(env[RUN_BUDGET_ENVIRONMENT_FLAG]) ?? settings.runBudgetSeconds;
|
|
208
216
|
}
|
|
209
217
|
|
|
218
|
+
/** Uses the same positive-integer environment parsing as the run budget. */
|
|
219
|
+
export function resolveMaxDetachedCells(settings: CodemodeSettings, env: Environment = process.env): number {
|
|
220
|
+
return (
|
|
221
|
+
positiveSecondsOverride(env[MAX_DETACHED_CELLS_ENVIRONMENT_FLAG]) ??
|
|
222
|
+
settings.maxDetachedCells ??
|
|
223
|
+
DEFAULT_MAX_DETACHED_CELLS
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
210
227
|
function positiveSecondsOverride(value: string | undefined): number | undefined {
|
|
211
228
|
if (value === undefined) return undefined;
|
|
212
229
|
const parsed = Number.parseInt(value, 10);
|
|
@@ -250,6 +267,7 @@ function mergeSettings(input: CodemodeSettingsInput): ResolvedCodemodeSettings {
|
|
|
250
267
|
foregroundWindowSeconds: input.foregroundWindowSeconds ?? defaultCodemodeSettings.foregroundWindowSeconds,
|
|
251
268
|
runBudgetSeconds: input.runBudgetSeconds ?? defaultCodemodeSettings.runBudgetSeconds,
|
|
252
269
|
hardLimitSeconds: input.hardLimitSeconds ?? defaultCodemodeSettings.hardLimitSeconds,
|
|
270
|
+
maxDetachedCells: input.maxDetachedCells ?? DEFAULT_MAX_DETACHED_CELLS,
|
|
253
271
|
parallelPoolWidth: input.parallelPoolWidth ?? defaultCodemodeSettings.parallelPoolWidth,
|
|
254
272
|
taskTools: {
|
|
255
273
|
task: input.taskTools?.task ?? defaultCodemodeSettings.taskTools.task,
|
|
@@ -28,7 +28,8 @@ function packLabels(labels: readonly string[], budget: number): string {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function labelOf(entry: EvalDetachedCellStatusEntry): string {
|
|
31
|
-
|
|
31
|
+
const label = entry.summary === undefined || entry.summary.length === 0 ? entry.cellId : entry.summary;
|
|
32
|
+
return entry.queuedBehind === undefined ? label : `queued ${label}`;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
/**
|
|
@@ -52,10 +53,12 @@ export function formatElapsedSeconds(value: number): string {
|
|
|
52
53
|
return `${hours}h ${remainingMinutes}m`;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
/** Whole seconds since the oldest detached cell
|
|
56
|
+
/** Whole seconds since the oldest executing detached cell started; queued cells do not tick. */
|
|
56
57
|
export function evalCellElapsedSeconds(entries: readonly EvalDetachedCellStatusEntry[], nowMs: number): number {
|
|
57
58
|
let oldest = Number.POSITIVE_INFINITY;
|
|
58
|
-
for (const entry of entries)
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (entry.queuedBehind === undefined) oldest = Math.min(oldest, entry.startedAtMs);
|
|
61
|
+
}
|
|
59
62
|
if (!Number.isFinite(oldest)) return 0;
|
|
60
63
|
return Math.max(0, Math.round((nowMs - oldest) / 1000));
|
|
61
64
|
}
|
|
@@ -67,7 +70,9 @@ export function formatEvalCellStatus(
|
|
|
67
70
|
): string | undefined {
|
|
68
71
|
const first = entries[0];
|
|
69
72
|
if (first === undefined) return undefined;
|
|
70
|
-
const suffix =
|
|
73
|
+
const suffix = entries.every((entry) => entry.queuedBehind !== undefined)
|
|
74
|
+
? " (queued)"
|
|
75
|
+
: ` (${formatElapsedSeconds(evalCellElapsedSeconds(entries, nowMs))})`;
|
|
71
76
|
if (entries.length === 1) {
|
|
72
77
|
const head = `${DETACHED_GLYPH} ${first.language} · `;
|
|
73
78
|
return head + truncateEnd(labelOf(first), MAX_STATUS_LENGTH - head.length - suffix.length) + suffix;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
defaultCodemodeSettings,
|
|
8
8
|
resolveForegroundWindowSeconds,
|
|
9
9
|
resolveHardLimitSeconds,
|
|
10
|
+
resolveMaxDetachedCells,
|
|
10
11
|
resolveRunBudgetSeconds,
|
|
11
12
|
} from "./config/settings.ts";
|
|
12
13
|
import { EvalNotifier } from "./extension/eval-notifier.ts";
|
|
@@ -181,6 +182,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
181
182
|
settings: defaultCodemodeSettings,
|
|
182
183
|
cellManager: new EvalDetachedCellManager({
|
|
183
184
|
notifier,
|
|
185
|
+
maxDetachedCells: resolveMaxDetachedCells(defaultCodemodeSettings),
|
|
184
186
|
hardLimitSeconds: resolveHardLimitSeconds(defaultCodemodeSettings),
|
|
185
187
|
runBudgetSeconds: resolveRunBudgetSeconds(defaultCodemodeSettings),
|
|
186
188
|
onStatusChange: showDetachedCells,
|
|
@@ -219,6 +221,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
219
221
|
const cellManager = new EvalDetachedCellManager({
|
|
220
222
|
artifactsDir: runtime.artifactsDir,
|
|
221
223
|
notifier,
|
|
224
|
+
maxDetachedCells: resolveMaxDetachedCells(runtime.settings),
|
|
222
225
|
hardLimitSeconds: resolveHardLimitSeconds(runtime.settings),
|
|
223
226
|
runBudgetSeconds: resolveRunBudgetSeconds(runtime.settings),
|
|
224
227
|
onStatusChange: showDetachedCells,
|
|
@@ -28,8 +28,8 @@ runner/prelude assets).
|
|
|
28
28
|
AST-parsed (Babel) and rewritten to bridge-compatible dynamic imports.
|
|
29
29
|
- JS runs on worker threads with an inline-worker fallback; py/rb/jl run as
|
|
30
30
|
framed subprocesses through `shared/`.
|
|
31
|
-
-
|
|
32
|
-
|
|
31
|
+
- Each language keeps one serial FIFO kernel. Queued cancellation removes only
|
|
32
|
+
that cell; active interruption and runtime recovery stay in the kernel layer.
|
|
33
33
|
- Every kernel exposes the active session's `PI_*` environment (`session-env.ts`):
|
|
34
34
|
inherited values are deleted before the session's values are applied, so any
|
|
35
35
|
child spawned from a cell sees the same session environment a bash-tool child
|
|
@@ -254,6 +254,12 @@ function output(ids...; format="raw", offset=nothing, limit=nothing)
|
|
|
254
254
|
senpi_with_bridge_timeout_pause(() -> senpi_call_tool("__output__", arguments))
|
|
255
255
|
end
|
|
256
256
|
|
|
257
|
+
"""
|
|
258
|
+
Delegate work with isolated/apply/merge; needs a host that supports isolation, otherwise a warning.
|
|
259
|
+
merge accepts "patch"/"branch" (false/true aliases). Unapplied foreground changes raise
|
|
260
|
+
an error with recovery instructions. Handles return immediately: await the completion
|
|
261
|
+
notification or read task_output for the isolation result.
|
|
262
|
+
"""
|
|
257
263
|
function agent(prompt::AbstractString; agent="task", model=nothing, label=nothing, schema=nothing, isolated=nothing, apply=nothing, merge=nothing, handle=false, kwargs...)
|
|
258
264
|
arguments = Dict{String, Any}("prompt" => string(prompt), "agent" => agent)
|
|
259
265
|
for (key, value) in (("model", model), ("label", label), ("schema", schema), ("isolated", isolated), ("apply", apply), ("merge", merge))
|
|
@@ -272,6 +278,10 @@ function agent(prompt::AbstractString; agent="task", model=nothing, label=nothin
|
|
|
272
278
|
result["handle"] = get(record, "handle", result["id"] === nothing ? nothing : "agent://" * string(result["id"]))
|
|
273
279
|
result["run_epoch"] = get(record, "run_epoch", nothing)
|
|
274
280
|
schema !== nothing && (result["data"] = parsed)
|
|
281
|
+
details = get(record, "details", nothing)
|
|
282
|
+
if details isa AbstractDict && haskey(details, "isolation")
|
|
283
|
+
result["details"] = Dict("isolation" => details["isolation"])
|
|
284
|
+
end
|
|
275
285
|
result
|
|
276
286
|
end
|
|
277
287
|
|