@code-yeongyu/senpi 2026.9.27-4 → 2026.9.28
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 +32 -0
- package/dist/bundle/chunks/{app-server-command-3GVJZFFH.js → app-server-command-TOPTGVQB.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-4SUOOZZ4.js → azure-openai-responses-FCZNBRWE.js} +1 -1
- package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
- package/dist/bundle/chunks/{chunk-PSCNHY7D.js → chunk-3GMFUEU7.js} +1 -1
- package/dist/bundle/chunks/{chunk-HBG6SQU5.js → chunk-62XHQXCZ.js} +1 -1
- package/dist/bundle/chunks/{chunk-SLL375O7.js → chunk-7DY2JPUX.js} +1 -1
- package/dist/bundle/chunks/chunk-7YUGCTEV.js +15 -0
- package/dist/bundle/chunks/{chunk-FWBSGMAE.js → chunk-A4UMPXQF.js} +1 -1
- package/dist/bundle/chunks/chunk-BBK6SSYT.js +2 -0
- package/dist/bundle/chunks/{chunk-C2425O2Q.js → chunk-CNQ4JB45.js} +1 -1
- package/dist/bundle/chunks/{chunk-27IFXX3B.js → chunk-DRN4FQCE.js} +1 -1
- package/dist/bundle/chunks/{chunk-22MGY5JQ.js → chunk-EHSZ7UCW.js} +1 -1
- package/dist/bundle/chunks/chunk-FHJYIU3U.js +5 -0
- package/dist/bundle/chunks/chunk-GJUCSTAG.js +19 -0
- package/dist/bundle/chunks/{chunk-VMKAYXHL.js → chunk-KIOTFUG6.js} +25 -20
- package/dist/bundle/chunks/{chunk-2BMD37YV.js → chunk-LUPPSEQW.js} +1 -1
- package/dist/bundle/chunks/chunk-OAIVTS5X.js +8 -0
- package/dist/bundle/chunks/{chunk-NRGMT74E.js → chunk-QOVUQZO4.js} +1 -1
- package/dist/bundle/chunks/chunk-SN4ZHPMN.js +135 -0
- package/dist/bundle/chunks/{chunk-IKXPBYSO.js → chunk-UJFLZ2M2.js} +1 -1
- package/dist/bundle/chunks/{chunk-4T4GFAGK.js → chunk-VFSXUT7K.js} +1 -1
- package/dist/bundle/chunks/{cli-main-66NFBKVH.js → cli-main-ARR54ET6.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-ZTGKZTGU.js → google-generative-ai-LFCBHG3Y.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-CCZ376YK.js → google-vertex-C474FXU7.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-NZ3I2UEN.js → help-fast-path-3U3YDEZ6.js} +1 -1
- package/dist/bundle/chunks/{host-command-RORPAC5X.js → host-command-6CJ7YM6W.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-UDMJGLOR.js → host-lifecycle-RZIMOFDM.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +6 -6
- package/dist/bundle/chunks/interactive-host-runtime-A3GEIFTU.js +4 -0
- package/dist/bundle/chunks/{interactive-mode-LOAADKLH.js → interactive-mode-ITSFLGF6.js} +1 -1
- package/dist/bundle/chunks/{list-tips-S5CANO5W.js → list-tips-FPXDXN7R.js} +1 -1
- package/dist/bundle/chunks/multi-session-host-B4WMZE2G.js +21 -0
- package/dist/bundle/chunks/{openai-codex-responses-YSGZZDD5.js → openai-codex-responses-2LDIFL6Z.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-KWHRWCBN.js → openai-completions-7KZLPKYP.js} +1 -1
- package/dist/bundle/chunks/{openai-images-6U5FCN5S.js → openai-images-I7DR7ERP.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-7PWMBGYX.js → openai-responses-LCHS3AF3.js} +1 -1
- package/dist/bundle/chunks/{openrouter-images-QIKWQZUD.js → openrouter-images-IPNDRK6C.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-5EEDA35I.js → package-manager-cli-DXHQ6RKA.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-KZG4OVR7.js → rotation-stream-ZVJEGA2G.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-HOEA5NGQ.js → rpc-mode-PFPNP65F.js} +1 -1
- package/dist/bundle/chunks/session-picker-KOPOV6Q4.js +2 -0
- package/dist/bundle/chunks/session-worker.js +311 -302
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/cli/args.js +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/host-command.d.ts +8 -1
- package/dist/cli/host-command.d.ts.map +1 -1
- package/dist/cli/host-command.js +66 -14
- package/dist/cli/host-command.js.map +1 -1
- package/dist/core/agent-session.d.ts +18 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +119 -3
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/retry-fallback/candidates.d.ts +26 -0
- package/dist/core/retry-fallback/candidates.d.ts.map +1 -0
- package/dist/core/retry-fallback/candidates.js +41 -0
- package/dist/core/retry-fallback/candidates.js.map +1 -0
- package/dist/core/retry-fallback/circuit-probes.d.ts +30 -0
- package/dist/core/retry-fallback/circuit-probes.d.ts.map +1 -0
- package/dist/core/retry-fallback/circuit-probes.js +60 -0
- package/dist/core/retry-fallback/circuit-probes.js.map +1 -0
- package/dist/core/retry-fallback/circuit.d.ts +101 -0
- package/dist/core/retry-fallback/circuit.d.ts.map +1 -0
- package/dist/core/retry-fallback/circuit.js +175 -0
- package/dist/core/retry-fallback/circuit.js.map +1 -0
- package/dist/core/retry-fallback/controller-types.d.ts +58 -0
- package/dist/core/retry-fallback/controller-types.d.ts.map +1 -0
- package/dist/core/retry-fallback/controller-types.js +2 -0
- package/dist/core/retry-fallback/controller-types.js.map +1 -0
- package/dist/core/retry-fallback/controller.d.ts +21 -49
- package/dist/core/retry-fallback/controller.d.ts.map +1 -1
- package/dist/core/retry-fallback/controller.js +88 -42
- package/dist/core/retry-fallback/controller.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +25 -17
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-failure-report.d.ts +33 -0
- package/dist/core/session-failure-report.d.ts.map +1 -0
- package/dist/core/session-failure-report.js +62 -0
- package/dist/core/session-failure-report.js.map +1 -0
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +4 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/index.d.ts +1 -0
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js +2 -0
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/interactive/help-content.d.ts.map +1 -1
- package/dist/modes/interactive/help-content.js +3 -0
- package/dist/modes/interactive/help-content.js.map +1 -1
- package/dist/modes/interactive/interactive-host-attach.d.ts +10 -0
- package/dist/modes/interactive/interactive-host-attach.d.ts.map +1 -0
- package/dist/modes/interactive/interactive-host-attach.js +14 -0
- package/dist/modes/interactive/interactive-host-attach.js.map +1 -0
- package/dist/modes/interactive/interactive-host-runtime.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-host-runtime.js +3 -6
- package/dist/modes/interactive/interactive-host-runtime.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +7 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +66 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/session-failure-info.d.ts +3 -0
- package/dist/modes/interactive/session-failure-info.d.ts.map +1 -0
- package/dist/modes/interactive/session-failure-info.js +21 -0
- package/dist/modes/interactive/session-failure-info.js.map +1 -0
- package/dist/modes/interactive/tips/catalog/model-tips.d.ts +7 -7
- package/dist/modes/interactive/tips/catalog/model-tips.d.ts.map +1 -1
- package/dist/modes/interactive/tips/catalog/model-tips.js +7 -1
- package/dist/modes/interactive/tips/catalog/model-tips.js.map +1 -1
- package/dist/modes/interactive/tips/catalog/session-tips.d.ts.map +1 -1
- package/dist/modes/interactive/tips/catalog/session-tips.js +1 -1
- package/dist/modes/interactive/tips/catalog/session-tips.js.map +1 -1
- package/dist/modes/rpc/host-attach-hold.d.ts +16 -0
- package/dist/modes/rpc/host-attach-hold.d.ts.map +1 -0
- package/dist/modes/rpc/host-attach-hold.js +9 -0
- package/dist/modes/rpc/host-attach-hold.js.map +1 -0
- package/dist/modes/rpc/host-cleanup-paths.d.ts +20 -0
- package/dist/modes/rpc/host-cleanup-paths.d.ts.map +1 -0
- package/dist/modes/rpc/host-cleanup-paths.js +13 -0
- package/dist/modes/rpc/host-cleanup-paths.js.map +1 -0
- package/dist/modes/rpc/host-client-occupancy.d.ts +24 -0
- package/dist/modes/rpc/host-client-occupancy.d.ts.map +1 -0
- package/dist/modes/rpc/host-client-occupancy.js +53 -0
- package/dist/modes/rpc/host-client-occupancy.js.map +1 -0
- package/dist/modes/rpc/host-daemon-paths.d.ts +61 -8
- package/dist/modes/rpc/host-daemon-paths.d.ts.map +1 -1
- package/dist/modes/rpc/host-daemon-paths.js +128 -11
- package/dist/modes/rpc/host-daemon-paths.js.map +1 -1
- package/dist/modes/rpc/host-daemon-registration.d.ts +11 -1
- package/dist/modes/rpc/host-daemon-registration.d.ts.map +1 -1
- package/dist/modes/rpc/host-daemon-registration.js +28 -15
- package/dist/modes/rpc/host-daemon-registration.js.map +1 -1
- package/dist/modes/rpc/host-endpoints.d.ts +12 -0
- package/dist/modes/rpc/host-endpoints.d.ts.map +1 -0
- package/dist/modes/rpc/host-endpoints.js +59 -0
- package/dist/modes/rpc/host-endpoints.js.map +1 -0
- package/dist/modes/rpc/host-ensure-lock.d.ts +11 -0
- package/dist/modes/rpc/host-ensure-lock.d.ts.map +1 -0
- package/dist/modes/rpc/host-ensure-lock.js +49 -0
- package/dist/modes/rpc/host-ensure-lock.js.map +1 -0
- package/dist/modes/rpc/host-ensure.d.ts +7 -0
- package/dist/modes/rpc/host-ensure.d.ts.map +1 -1
- package/dist/modes/rpc/host-ensure.js +42 -65
- package/dist/modes/rpc/host-ensure.js.map +1 -1
- package/dist/modes/rpc/host-gc-evidence.d.ts +18 -0
- package/dist/modes/rpc/host-gc-evidence.d.ts.map +1 -0
- package/dist/modes/rpc/host-gc-evidence.js +128 -0
- package/dist/modes/rpc/host-gc-evidence.js.map +1 -0
- package/dist/modes/rpc/host-gc.d.ts +28 -0
- package/dist/modes/rpc/host-gc.d.ts.map +1 -0
- package/dist/modes/rpc/host-gc.js +110 -0
- package/dist/modes/rpc/host-gc.js.map +1 -0
- package/dist/modes/rpc/host-generations.d.ts +10 -2
- package/dist/modes/rpc/host-generations.d.ts.map +1 -1
- package/dist/modes/rpc/host-generations.js +12 -7
- package/dist/modes/rpc/host-generations.js.map +1 -1
- package/dist/modes/rpc/host-handoff.d.ts +6 -0
- package/dist/modes/rpc/host-handoff.d.ts.map +1 -1
- package/dist/modes/rpc/host-handoff.js +31 -4
- package/dist/modes/rpc/host-handoff.js.map +1 -1
- package/dist/modes/rpc/host-lifecycle-policy.d.ts +53 -0
- package/dist/modes/rpc/host-lifecycle-policy.d.ts.map +1 -0
- package/dist/modes/rpc/host-lifecycle-policy.js +64 -0
- package/dist/modes/rpc/host-lifecycle-policy.js.map +1 -0
- package/dist/modes/rpc/host-lifecycle.d.ts +1 -44
- package/dist/modes/rpc/host-lifecycle.d.ts.map +1 -1
- package/dist/modes/rpc/host-lifecycle.js +23 -85
- package/dist/modes/rpc/host-lifecycle.js.map +1 -1
- package/dist/modes/rpc/host-observe-request.d.ts +17 -0
- package/dist/modes/rpc/host-observe-request.d.ts.map +1 -0
- package/dist/modes/rpc/host-observe-request.js +30 -0
- package/dist/modes/rpc/host-observe-request.js.map +1 -0
- package/dist/modes/rpc/host-probe.d.ts +12 -0
- package/dist/modes/rpc/host-probe.d.ts.map +1 -1
- package/dist/modes/rpc/host-probe.js +28 -1
- package/dist/modes/rpc/host-probe.js.map +1 -1
- package/dist/modes/rpc/host-protocol-info.d.ts +2 -0
- package/dist/modes/rpc/host-protocol-info.d.ts.map +1 -1
- package/dist/modes/rpc/host-protocol-info.js +1 -0
- package/dist/modes/rpc/host-protocol-info.js.map +1 -1
- package/dist/modes/rpc/host-readiness.d.ts +22 -0
- package/dist/modes/rpc/host-readiness.d.ts.map +1 -0
- package/dist/modes/rpc/host-readiness.js +44 -0
- package/dist/modes/rpc/host-readiness.js.map +1 -0
- package/dist/modes/rpc/host-runner.d.ts +13 -0
- package/dist/modes/rpc/host-runner.d.ts.map +1 -1
- package/dist/modes/rpc/host-runner.js +23 -4
- package/dist/modes/rpc/host-runner.js.map +1 -1
- package/dist/modes/rpc/host-status-all.d.ts +31 -0
- package/dist/modes/rpc/host-status-all.d.ts.map +1 -0
- package/dist/modes/rpc/host-status-all.js +78 -0
- package/dist/modes/rpc/host-status-all.js.map +1 -0
- package/dist/modes/rpc/host-status-rows.d.ts +35 -0
- package/dist/modes/rpc/host-status-rows.d.ts.map +1 -0
- package/dist/modes/rpc/host-status-rows.js +42 -0
- package/dist/modes/rpc/host-status-rows.js.map +1 -0
- package/dist/modes/rpc/host-status.d.ts +32 -1
- package/dist/modes/rpc/host-status.d.ts.map +1 -1
- package/dist/modes/rpc/host-status.js +33 -27
- package/dist/modes/rpc/host-status.js.map +1 -1
- package/dist/modes/rpc/host-successor.d.ts +3 -1
- package/dist/modes/rpc/host-successor.d.ts.map +1 -1
- package/dist/modes/rpc/host-successor.js +105 -42
- package/dist/modes/rpc/host-successor.js.map +1 -1
- package/dist/modes/rpc/host-watchdog.d.ts +6 -0
- package/dist/modes/rpc/host-watchdog.d.ts.map +1 -1
- package/dist/modes/rpc/host-watchdog.js +12 -13
- package/dist/modes/rpc/host-watchdog.js.map +1 -1
- package/dist/modes/rpc/loop-lag-watchdog.d.ts +16 -0
- package/dist/modes/rpc/loop-lag-watchdog.d.ts.map +1 -1
- package/dist/modes/rpc/loop-lag-watchdog.js +34 -2
- package/dist/modes/rpc/loop-lag-watchdog.js.map +1 -1
- package/dist/modes/rpc/multi-session-host.d.ts +2 -1
- package/dist/modes/rpc/multi-session-host.d.ts.map +1 -1
- package/dist/modes/rpc/multi-session-host.js +33 -6
- package/dist/modes/rpc/multi-session-host.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts +2 -2
- package/dist/modes/rpc/rpc-mode.js +2 -2
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +16 -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 +9 -1
- package/dist/modes/rpc/session-command-router.d.ts.map +1 -1
- package/dist/modes/rpc/session-command-router.js +4 -1
- package/dist/modes/rpc/session-command-router.js.map +1 -1
- package/dist/modes/rpc/session-path-key.d.ts +3 -1
- package/dist/modes/rpc/session-path-key.d.ts.map +1 -1
- package/dist/modes/rpc/session-path-key.js +3 -1
- package/dist/modes/rpc/session-path-key.js.map +1 -1
- package/docs/rpc.md +186 -19
- package/docs/settings.md +25 -1
- package/docs/usage.md +5 -2
- package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +12 -0
- package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
- package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.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/utils/error-body.d.ts +6 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.js +36 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.js.map +1 -1
- 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 +14 -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-3DEXCYNN.js +0 -135
- package/dist/bundle/chunks/chunk-5X4KSPK5.js +0 -12
- package/dist/bundle/chunks/chunk-KMD5ZONU.js +0 -5
- package/dist/bundle/chunks/chunk-PEHZYGYA.js +0 -8
- package/dist/bundle/chunks/chunk-PRC5XIIH.js +0 -2
- package/dist/bundle/chunks/chunk-QKY33652.js +0 -18
- package/dist/bundle/chunks/interactive-host-runtime-TIFT2SKR.js +0 -4
- package/dist/bundle/chunks/multi-session-host-GRHKB7Q2.js +0 -21
- package/dist/bundle/chunks/session-picker-FTFCQYHH.js +0 -2
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
3
|
+
import { type FallbackSelector } from "./chains.ts";
|
|
4
|
+
export interface CandidateRegistry {
|
|
5
|
+
find(provider: string, id: string): Model<Api> | undefined;
|
|
6
|
+
getAll(): Model<Api>[];
|
|
7
|
+
}
|
|
8
|
+
export interface CandidateFilters {
|
|
9
|
+
registry: CandidateRegistry;
|
|
10
|
+
tried: ReadonlySet<string>;
|
|
11
|
+
isSuppressed(base: string): boolean;
|
|
12
|
+
isAuthAvailable(provider: string): boolean;
|
|
13
|
+
isCircuitOpen(base: string): boolean;
|
|
14
|
+
skip(candidate: string, skipReason: string): void;
|
|
15
|
+
}
|
|
16
|
+
export interface UsableCandidate {
|
|
17
|
+
selector: FallbackSelector;
|
|
18
|
+
model: Model<Api>;
|
|
19
|
+
/** True only when every usable entry is circuit-open: the chain then probes instead of refusing. */
|
|
20
|
+
circuitOpen: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function firstUsableCandidate(entries: readonly string[], current: {
|
|
23
|
+
model: Model<Api>;
|
|
24
|
+
thinkingLevel?: ThinkingLevel;
|
|
25
|
+
}, filters: CandidateFilters): UsableCandidate | undefined;
|
|
26
|
+
//# sourceMappingURL=candidates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"candidates.d.ts","sourceRoot":"","sources":["../../../src/core/retry-fallback/candidates.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAGN,KAAK,gBAAgB,EAGrB,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,iBAAiB;IACjC,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IAC3D,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAChC,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3B,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACpC,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3C,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACrC,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAClD;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,oGAAoG;IACpG,WAAW,EAAE,OAAO,CAAC;CACrB;AAED,wBAAgB,oBAAoB,CACnC,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,OAAO,EAAE;IAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAAC,aAAa,CAAC,EAAE,aAAa,CAAA;CAAE,EAC7D,OAAO,EAAE,gBAAgB,GACvB,eAAe,GAAG,SAAS,CAsC7B"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { baseSelector, candidatesAfter, formatSelector, parseFallbackSelector, } from "./chains.js";
|
|
2
|
+
export function firstUsableCandidate(entries, current, filters) {
|
|
3
|
+
let probe;
|
|
4
|
+
for (const raw of candidatesAfter(entries, formatSelector(current.model, current.thinkingLevel))) {
|
|
5
|
+
const selector = parseFallbackSelector(raw, filters.registry);
|
|
6
|
+
if (!selector) {
|
|
7
|
+
filters.skip(raw, "unknown");
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
if (selector.provider === current.model.provider && selector.id === current.model.id) {
|
|
11
|
+
filters.skip(raw, "self");
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const base = baseSelector(selector);
|
|
15
|
+
if (filters.tried.has(base)) {
|
|
16
|
+
filters.skip(raw, "tried");
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (filters.isSuppressed(base)) {
|
|
20
|
+
filters.skip(raw, "suppressed");
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (!filters.isAuthAvailable(selector.provider)) {
|
|
24
|
+
filters.skip(raw, "unauthenticated");
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const model = filters.registry.find(selector.provider, selector.id);
|
|
28
|
+
if (!model) {
|
|
29
|
+
filters.skip(raw, "unknown");
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (filters.isCircuitOpen(base)) {
|
|
33
|
+
filters.skip(raw, "circuit-open");
|
|
34
|
+
probe ??= { selector, model, circuitOpen: true };
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
return { selector, model, circuitOpen: false };
|
|
38
|
+
}
|
|
39
|
+
return probe;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=candidates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"candidates.js","sourceRoot":"","sources":["../../../src/core/retry-fallback/candidates.ts"],"names":[],"mappings":"AAEA,OAAO,EACN,YAAY,EACZ,eAAe,EAEf,cAAc,EACd,qBAAqB,GACrB,MAAM,aAAa,CAAC;AAuBrB,MAAM,UAAU,oBAAoB,CACnC,OAA0B,EAC1B,OAA6D,EAC7D,OAAyB;IAEzB,IAAI,KAAkC,CAAC;IACvC,KAAK,MAAM,GAAG,IAAI,eAAe,CAAC,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC;QAClG,MAAM,QAAQ,GAAG,qBAAqB,CAAC,GAAG,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC9D,IAAI,CAAC,QAAQ,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;YAC7B,SAAS;QACV,CAAC;QACD,IAAI,QAAQ,CAAC,QAAQ,KAAK,OAAO,CAAC,KAAK,CAAC,QAAQ,IAAI,QAAQ,CAAC,EAAE,KAAK,OAAO,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;YACtF,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YAC1B,SAAS;QACV,CAAC;QACD,MAAM,IAAI,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;QACpC,IAAI,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7B,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YAC3B,SAAS;QACV,CAAC;QACD,IAAI,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;YAChC,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;YAChC,SAAS;QACV,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YACjD,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;YACrC,SAAS;QACV,CAAC;QACD,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,KAAK,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;YAC7B,SAAS;QACV,CAAC;QACD,IAAI,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;YACjC,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;YAClC,KAAK,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;YACjD,SAAS;QACV,CAAC;QACD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;IAChD,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC","sourcesContent":["import type { ThinkingLevel } from \"@earendil-works/pi-agent-core\";\nimport type { Api, Model } from \"@earendil-works/pi-ai\";\nimport {\n\tbaseSelector,\n\tcandidatesAfter,\n\ttype FallbackSelector,\n\tformatSelector,\n\tparseFallbackSelector,\n} from \"./chains.ts\";\n\nexport interface CandidateRegistry {\n\tfind(provider: string, id: string): Model<Api> | undefined;\n\tgetAll(): Model<Api>[];\n}\n\nexport interface CandidateFilters {\n\tregistry: CandidateRegistry;\n\ttried: ReadonlySet<string>;\n\tisSuppressed(base: string): boolean;\n\tisAuthAvailable(provider: string): boolean;\n\tisCircuitOpen(base: string): boolean;\n\tskip(candidate: string, skipReason: string): void;\n}\n\nexport interface UsableCandidate {\n\tselector: FallbackSelector;\n\tmodel: Model<Api>;\n\t/** True only when every usable entry is circuit-open: the chain then probes instead of refusing. */\n\tcircuitOpen: boolean;\n}\n\nexport function firstUsableCandidate(\n\tentries: readonly string[],\n\tcurrent: { model: Model<Api>; thinkingLevel?: ThinkingLevel },\n\tfilters: CandidateFilters,\n): UsableCandidate | undefined {\n\tlet probe: UsableCandidate | undefined;\n\tfor (const raw of candidatesAfter(entries, formatSelector(current.model, current.thinkingLevel))) {\n\t\tconst selector = parseFallbackSelector(raw, filters.registry);\n\t\tif (!selector) {\n\t\t\tfilters.skip(raw, \"unknown\");\n\t\t\tcontinue;\n\t\t}\n\t\tif (selector.provider === current.model.provider && selector.id === current.model.id) {\n\t\t\tfilters.skip(raw, \"self\");\n\t\t\tcontinue;\n\t\t}\n\t\tconst base = baseSelector(selector);\n\t\tif (filters.tried.has(base)) {\n\t\t\tfilters.skip(raw, \"tried\");\n\t\t\tcontinue;\n\t\t}\n\t\tif (filters.isSuppressed(base)) {\n\t\t\tfilters.skip(raw, \"suppressed\");\n\t\t\tcontinue;\n\t\t}\n\t\tif (!filters.isAuthAvailable(selector.provider)) {\n\t\t\tfilters.skip(raw, \"unauthenticated\");\n\t\t\tcontinue;\n\t\t}\n\t\tconst model = filters.registry.find(selector.provider, selector.id);\n\t\tif (!model) {\n\t\t\tfilters.skip(raw, \"unknown\");\n\t\t\tcontinue;\n\t\t}\n\t\tif (filters.isCircuitOpen(base)) {\n\t\t\tfilters.skip(raw, \"circuit-open\");\n\t\t\tprobe ??= { selector, model, circuitOpen: true };\n\t\t\tcontinue;\n\t\t}\n\t\treturn { selector, model, circuitOpen: false };\n\t}\n\treturn probe;\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CircuitAdmission, FallbackCircuitAccess } from "./circuit.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Non-retryable failures that still say the entry is unusable for everyone:
|
|
4
|
+
* billing and account quota/budget exhaustion. Auth and request-shape rejections
|
|
5
|
+
* are not outages and never open a circuit.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isHealthExhaustionFailure(errorMessage: string | undefined): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* One session's side of the shared breaker: at most one probe token, taken by
|
|
10
|
+
* atomic admission before the session switches to (or starts on) an entry, and
|
|
11
|
+
* given back on every terminal path - accepted, failed, aborted, or abandoned.
|
|
12
|
+
*/
|
|
13
|
+
export declare class CircuitProbes {
|
|
14
|
+
private readonly circuits;
|
|
15
|
+
private token;
|
|
16
|
+
constructor(circuits: FallbackCircuitAccess | undefined);
|
|
17
|
+
isOpen(selector: string): boolean;
|
|
18
|
+
governs(selector: string): boolean;
|
|
19
|
+
admit(selector: string): CircuitAdmission["kind"];
|
|
20
|
+
holds(selector: string): boolean;
|
|
21
|
+
get probing(): string | undefined;
|
|
22
|
+
noteFailure(selector: string, failure: {
|
|
23
|
+
retryAfterMs?: number;
|
|
24
|
+
errorMessage?: string;
|
|
25
|
+
}): void;
|
|
26
|
+
accept(selector: string): void;
|
|
27
|
+
release(): void;
|
|
28
|
+
releaseAll(): void;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=circuit-probes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-probes.d.ts","sourceRoot":"","sources":["../../../src/core/retry-fallback/circuit-probes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAc,MAAM,cAAc,CAAC;AAExF;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEnF;AAED;;;;GAIG;AACH,qBAAa,aAAa;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoC;IAC7D,OAAO,CAAC,KAAK,CAAyB;IAEtC,YAAY,QAAQ,EAAE,qBAAqB,GAAG,SAAS,EAEtD;IAED,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEhC;IAED,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEjC;IAED,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAOhD;IAED,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE/B;IAED,IAAI,OAAO,IAAI,MAAM,GAAG,SAAS,CAEhC;IAED,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAG7F;IAED,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAG7B;IAED,OAAO,IAAI,IAAI,CAGd;IAED,UAAU,IAAI,IAAI,CAGjB;CACD"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { isQuotaExhaustionMessage } from "@earendil-works/pi-ai";
|
|
2
|
+
import { isBillingErrorMessage } from "./billing.js";
|
|
3
|
+
/**
|
|
4
|
+
* Non-retryable failures that still say the entry is unusable for everyone:
|
|
5
|
+
* billing and account quota/budget exhaustion. Auth and request-shape rejections
|
|
6
|
+
* are not outages and never open a circuit.
|
|
7
|
+
*/
|
|
8
|
+
export function isHealthExhaustionFailure(errorMessage) {
|
|
9
|
+
return isBillingErrorMessage(errorMessage) || isQuotaExhaustionMessage(errorMessage);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* One session's side of the shared breaker: at most one probe token, taken by
|
|
13
|
+
* atomic admission before the session switches to (or starts on) an entry, and
|
|
14
|
+
* given back on every terminal path - accepted, failed, aborted, or abandoned.
|
|
15
|
+
*/
|
|
16
|
+
export class CircuitProbes {
|
|
17
|
+
constructor(circuits) {
|
|
18
|
+
this.circuits = circuits;
|
|
19
|
+
}
|
|
20
|
+
isOpen(selector) {
|
|
21
|
+
return this.circuits?.isOpen(selector) ?? false;
|
|
22
|
+
}
|
|
23
|
+
governs(selector) {
|
|
24
|
+
return this.circuits?.governs(selector) ?? false;
|
|
25
|
+
}
|
|
26
|
+
admit(selector) {
|
|
27
|
+
const admission = this.circuits?.admit(selector) ?? { kind: "closed" };
|
|
28
|
+
if (admission.kind === "probe" && this.token?.selector !== selector) {
|
|
29
|
+
this.release();
|
|
30
|
+
this.token = admission.token;
|
|
31
|
+
}
|
|
32
|
+
return admission.kind;
|
|
33
|
+
}
|
|
34
|
+
holds(selector) {
|
|
35
|
+
return this.token?.selector === selector;
|
|
36
|
+
}
|
|
37
|
+
get probing() {
|
|
38
|
+
return this.token?.selector;
|
|
39
|
+
}
|
|
40
|
+
noteFailure(selector, failure) {
|
|
41
|
+
this.circuits?.noteFailure(selector, failure);
|
|
42
|
+
if (this.token?.selector === selector)
|
|
43
|
+
this.token = undefined;
|
|
44
|
+
}
|
|
45
|
+
accept(selector) {
|
|
46
|
+
this.circuits?.close(selector);
|
|
47
|
+
if (this.token?.selector === selector)
|
|
48
|
+
this.token = undefined;
|
|
49
|
+
}
|
|
50
|
+
release() {
|
|
51
|
+
if (this.token)
|
|
52
|
+
this.circuits?.release(this.token);
|
|
53
|
+
this.token = undefined;
|
|
54
|
+
}
|
|
55
|
+
releaseAll() {
|
|
56
|
+
this.token = undefined;
|
|
57
|
+
this.circuits?.releaseAll();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=circuit-probes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-probes.js","sourceRoot":"","sources":["../../../src/core/retry-fallback/circuit-probes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAGrD;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,YAAgC;IACzE,OAAO,qBAAqB,CAAC,YAAY,CAAC,IAAI,wBAAwB,CAAC,YAAY,CAAC,CAAC;AACtF,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,aAAa;IAIzB,YAAY,QAA2C;QACtD,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC1B,CAAC;IAED,MAAM,CAAC,QAAgB;QACtB,OAAO,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC;IACjD,CAAC;IAED,OAAO,CAAC,QAAgB;QACvB,OAAO,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC;IAClD,CAAC;IAED,KAAK,CAAC,QAAgB;QACrB,MAAM,SAAS,GAAqB,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;QACzF,IAAI,SAAS,CAAC,IAAI,KAAK,OAAO,IAAI,IAAI,CAAC,KAAK,EAAE,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACrE,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC;QAC9B,CAAC;QACD,OAAO,SAAS,CAAC,IAAI,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,QAAgB;QACrB,OAAO,IAAI,CAAC,KAAK,EAAE,QAAQ,KAAK,QAAQ,CAAC;IAC1C,CAAC;IAED,IAAI,OAAO;QACV,OAAO,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC;IAC7B,CAAC;IAED,WAAW,CAAC,QAAgB,EAAE,OAAyD;QACtF,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC9C,IAAI,IAAI,CAAC,KAAK,EAAE,QAAQ,KAAK,QAAQ;YAAE,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;IAC/D,CAAC;IAED,MAAM,CAAC,QAAgB;QACtB,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC/B,IAAI,IAAI,CAAC,KAAK,EAAE,QAAQ,KAAK,QAAQ;YAAE,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;IAC/D,CAAC;IAED,OAAO;QACN,IAAI,IAAI,CAAC,KAAK;YAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACnD,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;IACxB,CAAC;IAED,UAAU;QACT,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;QACvB,IAAI,CAAC,QAAQ,EAAE,UAAU,EAAE,CAAC;IAC7B,CAAC;CACD","sourcesContent":["import { isQuotaExhaustionMessage } from \"@earendil-works/pi-ai\";\nimport { isBillingErrorMessage } from \"./billing.ts\";\nimport type { CircuitAdmission, FallbackCircuitAccess, ProbeToken } from \"./circuit.ts\";\n\n/**\n * Non-retryable failures that still say the entry is unusable for everyone:\n * billing and account quota/budget exhaustion. Auth and request-shape rejections\n * are not outages and never open a circuit.\n */\nexport function isHealthExhaustionFailure(errorMessage: string | undefined): boolean {\n\treturn isBillingErrorMessage(errorMessage) || isQuotaExhaustionMessage(errorMessage);\n}\n\n/**\n * One session's side of the shared breaker: at most one probe token, taken by\n * atomic admission before the session switches to (or starts on) an entry, and\n * given back on every terminal path - accepted, failed, aborted, or abandoned.\n */\nexport class CircuitProbes {\n\tprivate readonly circuits: FallbackCircuitAccess | undefined;\n\tprivate token: ProbeToken | undefined;\n\n\tconstructor(circuits: FallbackCircuitAccess | undefined) {\n\t\tthis.circuits = circuits;\n\t}\n\n\tisOpen(selector: string): boolean {\n\t\treturn this.circuits?.isOpen(selector) ?? false;\n\t}\n\n\tgoverns(selector: string): boolean {\n\t\treturn this.circuits?.governs(selector) ?? false;\n\t}\n\n\tadmit(selector: string): CircuitAdmission[\"kind\"] {\n\t\tconst admission: CircuitAdmission = this.circuits?.admit(selector) ?? { kind: \"closed\" };\n\t\tif (admission.kind === \"probe\" && this.token?.selector !== selector) {\n\t\t\tthis.release();\n\t\t\tthis.token = admission.token;\n\t\t}\n\t\treturn admission.kind;\n\t}\n\n\tholds(selector: string): boolean {\n\t\treturn this.token?.selector === selector;\n\t}\n\n\tget probing(): string | undefined {\n\t\treturn this.token?.selector;\n\t}\n\n\tnoteFailure(selector: string, failure: { retryAfterMs?: number; errorMessage?: string }): void {\n\t\tthis.circuits?.noteFailure(selector, failure);\n\t\tif (this.token?.selector === selector) this.token = undefined;\n\t}\n\n\taccept(selector: string): void {\n\t\tthis.circuits?.close(selector);\n\t\tif (this.token?.selector === selector) this.token = undefined;\n\t}\n\n\trelease(): void {\n\t\tif (this.token) this.circuits?.release(this.token);\n\t\tthis.token = undefined;\n\t}\n\n\treleaseAll(): void {\n\t\tthis.token = undefined;\n\t\tthis.circuits?.releaseAll();\n\t}\n}\n"]}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { FallbackLogger } from "./log.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Fallback-chain circuit breaker. An entry that fails out of a configured chain
|
|
4
|
+
* opens a circuit keyed by its base selector (`provider/id`); while it is open,
|
|
5
|
+
* chain resolution skips the entry instead of spending its retry budget again.
|
|
6
|
+
*
|
|
7
|
+
* One breaker is shared by every session in the process that uses the same agent
|
|
8
|
+
* directory - the config that defines the providers and selectors - so a session
|
|
9
|
+
* started by `/new`, `/resume`, or `/fork` (each gets a fresh model runtime) and
|
|
10
|
+
* an in-process subagent do not re-discover a dead provider.
|
|
11
|
+
* The class is clock-free: every call receives `now`, keeping sessions' injected
|
|
12
|
+
* clocks authoritative and tests deterministic.
|
|
13
|
+
*/
|
|
14
|
+
export declare const DEFAULT_CIRCUIT_COOLDOWN_MS = 60000;
|
|
15
|
+
export declare const DEFAULT_CIRCUIT_MAX_COOLDOWN_MS: number;
|
|
16
|
+
export interface FallbackCircuitSettings {
|
|
17
|
+
/** First cooldown after an entry fails out of a chain; doubles per consecutive open. 0 disables the breaker. */
|
|
18
|
+
circuitCooldownMs?: number;
|
|
19
|
+
circuitMaxCooldownMs?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface ResolvedFallbackCircuitSettings {
|
|
22
|
+
cooldownMs: number;
|
|
23
|
+
maxCooldownMs: number;
|
|
24
|
+
}
|
|
25
|
+
export interface CircuitOpenRequest {
|
|
26
|
+
now: number;
|
|
27
|
+
cooldownMs: number;
|
|
28
|
+
maxCooldownMs: number;
|
|
29
|
+
retryAfterMs?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface ProbeToken {
|
|
32
|
+
readonly selector: string;
|
|
33
|
+
readonly owner: string;
|
|
34
|
+
readonly generation: number;
|
|
35
|
+
}
|
|
36
|
+
export type CircuitAdmission = {
|
|
37
|
+
kind: "closed";
|
|
38
|
+
} | {
|
|
39
|
+
kind: "open";
|
|
40
|
+
} | {
|
|
41
|
+
kind: "probe";
|
|
42
|
+
token: ProbeToken;
|
|
43
|
+
};
|
|
44
|
+
export declare function resolveFallbackCircuitSettings(settings: FallbackCircuitSettings | undefined): ResolvedFallbackCircuitSettings;
|
|
45
|
+
export declare class FallbackCircuitBreaker {
|
|
46
|
+
private readonly circuits;
|
|
47
|
+
private generations;
|
|
48
|
+
get size(): number;
|
|
49
|
+
open(selector: string, request: CircuitOpenRequest): number;
|
|
50
|
+
isOpen(selector: string, now: number, owner: string): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Atomic admission: a closed selector passes, a cooling one or one whose probe
|
|
53
|
+
* another requester holds is refused, and a half-open one hands `owner` its
|
|
54
|
+
* single probe under a fresh generation. The probe stays exclusive until its
|
|
55
|
+
* token is released or the circuit settles - it never expires on a timer, so a
|
|
56
|
+
* slow probe cannot overlap a second one, and a stale token never releases a
|
|
57
|
+
* later acquisition.
|
|
58
|
+
*/
|
|
59
|
+
admit(selector: string, now: number, owner: string): CircuitAdmission;
|
|
60
|
+
release(token: ProbeToken): void;
|
|
61
|
+
releaseOwnersWithPrefix(prefix: string): void;
|
|
62
|
+
close(selector: string): void;
|
|
63
|
+
has(selector: string): boolean;
|
|
64
|
+
sweep(now: number): void;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A session's requests are admitted per lane, not per session: the foreground
|
|
68
|
+
* turn is one lane, each background probe-back its own. Two lanes of one session
|
|
69
|
+
* never share a probe, so a probe-back in flight keeps the foreground turn off
|
|
70
|
+
* the entry exactly as it keeps a sibling session off.
|
|
71
|
+
*/
|
|
72
|
+
export type CircuitLane = "turn" | `probe-back:${number}`;
|
|
73
|
+
export interface FallbackCircuitAccess {
|
|
74
|
+
noteFailure(selector: string, failure: {
|
|
75
|
+
retryAfterMs?: number;
|
|
76
|
+
errorMessage?: string;
|
|
77
|
+
}): void;
|
|
78
|
+
/** Whether the breaker holds any state for the selector, i.e. it, not a session cooldown, governs recovery. */
|
|
79
|
+
governs(selector: string): boolean;
|
|
80
|
+
isOpen(selector: string, lane?: CircuitLane): boolean;
|
|
81
|
+
admit(selector: string, lane?: CircuitLane): CircuitAdmission;
|
|
82
|
+
release(token: ProbeToken): void;
|
|
83
|
+
releaseAll(): void;
|
|
84
|
+
close(selector: string): void;
|
|
85
|
+
}
|
|
86
|
+
export interface FallbackCircuitAccessDeps {
|
|
87
|
+
breaker: FallbackCircuitBreaker;
|
|
88
|
+
owner(): string;
|
|
89
|
+
now(): number;
|
|
90
|
+
settings(): ResolvedFallbackCircuitSettings;
|
|
91
|
+
logger: FallbackLogger;
|
|
92
|
+
}
|
|
93
|
+
export declare function createFallbackCircuitAccess(deps: FallbackCircuitAccessDeps): FallbackCircuitAccess;
|
|
94
|
+
export declare function fallbackCircuitsFor(agentDir: string): FallbackCircuitBreaker;
|
|
95
|
+
/** Holds the agent dir's breaker for one session; `release` is idempotent. */
|
|
96
|
+
export declare function acquireFallbackCircuits(agentDir: string): {
|
|
97
|
+
breaker: FallbackCircuitBreaker;
|
|
98
|
+
release(): void;
|
|
99
|
+
};
|
|
100
|
+
export declare function monotonicNow(): number;
|
|
101
|
+
//# sourceMappingURL=circuit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit.d.ts","sourceRoot":"","sources":["../../../src/core/retry-fallback/circuit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { parseRetryAfterMsMarker } from "@earendil-works/pi-ai/utils/retry-hint";
|
|
3
|
+
/**
|
|
4
|
+
* Fallback-chain circuit breaker. An entry that fails out of a configured chain
|
|
5
|
+
* opens a circuit keyed by its base selector (`provider/id`); while it is open,
|
|
6
|
+
* chain resolution skips the entry instead of spending its retry budget again.
|
|
7
|
+
*
|
|
8
|
+
* One breaker is shared by every session in the process that uses the same agent
|
|
9
|
+
* directory - the config that defines the providers and selectors - so a session
|
|
10
|
+
* started by `/new`, `/resume`, or `/fork` (each gets a fresh model runtime) and
|
|
11
|
+
* an in-process subagent do not re-discover a dead provider.
|
|
12
|
+
* The class is clock-free: every call receives `now`, keeping sessions' injected
|
|
13
|
+
* clocks authoritative and tests deterministic.
|
|
14
|
+
*/
|
|
15
|
+
export const DEFAULT_CIRCUIT_COOLDOWN_MS = 60_000;
|
|
16
|
+
export const DEFAULT_CIRCUIT_MAX_COOLDOWN_MS = 30 * 60_000;
|
|
17
|
+
function nonNegativeMs(value, fallback) {
|
|
18
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
19
|
+
}
|
|
20
|
+
export function resolveFallbackCircuitSettings(settings) {
|
|
21
|
+
const cooldownMs = nonNegativeMs(settings?.circuitCooldownMs, DEFAULT_CIRCUIT_COOLDOWN_MS);
|
|
22
|
+
const maxCooldownMs = nonNegativeMs(settings?.circuitMaxCooldownMs, DEFAULT_CIRCUIT_MAX_COOLDOWN_MS);
|
|
23
|
+
return { cooldownMs, maxCooldownMs: Math.max(cooldownMs, maxCooldownMs) };
|
|
24
|
+
}
|
|
25
|
+
export class FallbackCircuitBreaker {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.circuits = new Map();
|
|
28
|
+
this.generations = 0;
|
|
29
|
+
}
|
|
30
|
+
get size() {
|
|
31
|
+
return this.circuits.size;
|
|
32
|
+
}
|
|
33
|
+
open(selector, request) {
|
|
34
|
+
const previous = this.circuits.get(selector);
|
|
35
|
+
const consecutiveOpens = (previous?.consecutiveOpens ?? 0) + 1;
|
|
36
|
+
const cooldownMs = Math.min(request.cooldownMs * 2 ** (consecutiveOpens - 1), request.maxCooldownMs);
|
|
37
|
+
const retryAfterMs = request.retryAfterMs;
|
|
38
|
+
const hinted = retryAfterMs !== undefined && Number.isFinite(retryAfterMs) && retryAfterMs > 0
|
|
39
|
+
? request.now + retryAfterMs
|
|
40
|
+
: 0;
|
|
41
|
+
const retryFloorUntil = Math.max(hinted, previous && previous.retryFloorUntil > request.now ? previous.retryFloorUntil : 0);
|
|
42
|
+
const openUntil = Math.max(request.now + cooldownMs, retryFloorUntil);
|
|
43
|
+
this.circuits.set(selector, {
|
|
44
|
+
openUntil,
|
|
45
|
+
retryFloorUntil,
|
|
46
|
+
consecutiveOpens,
|
|
47
|
+
cooldownMs,
|
|
48
|
+
maxCooldownMs: request.maxCooldownMs,
|
|
49
|
+
probe: undefined,
|
|
50
|
+
});
|
|
51
|
+
return openUntil;
|
|
52
|
+
}
|
|
53
|
+
isOpen(selector, now, owner) {
|
|
54
|
+
const circuit = this.circuits.get(selector);
|
|
55
|
+
if (!circuit)
|
|
56
|
+
return false;
|
|
57
|
+
if (now < circuit.openUntil)
|
|
58
|
+
return true;
|
|
59
|
+
return circuit.probe !== undefined && circuit.probe.owner !== owner;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Atomic admission: a closed selector passes, a cooling one or one whose probe
|
|
63
|
+
* another requester holds is refused, and a half-open one hands `owner` its
|
|
64
|
+
* single probe under a fresh generation. The probe stays exclusive until its
|
|
65
|
+
* token is released or the circuit settles - it never expires on a timer, so a
|
|
66
|
+
* slow probe cannot overlap a second one, and a stale token never releases a
|
|
67
|
+
* later acquisition.
|
|
68
|
+
*/
|
|
69
|
+
admit(selector, now, owner) {
|
|
70
|
+
const circuit = this.circuits.get(selector);
|
|
71
|
+
if (!circuit)
|
|
72
|
+
return { kind: "closed" };
|
|
73
|
+
if (now < circuit.openUntil)
|
|
74
|
+
return { kind: "open" };
|
|
75
|
+
if (circuit.probe && circuit.probe.owner !== owner)
|
|
76
|
+
return { kind: "open" };
|
|
77
|
+
circuit.probe ??= { owner, generation: ++this.generations };
|
|
78
|
+
return { kind: "probe", token: { selector, owner, generation: circuit.probe.generation } };
|
|
79
|
+
}
|
|
80
|
+
release(token) {
|
|
81
|
+
const probe = this.circuits.get(token.selector)?.probe;
|
|
82
|
+
if (probe && probe.owner === token.owner && probe.generation === token.generation) {
|
|
83
|
+
const circuit = this.circuits.get(token.selector);
|
|
84
|
+
if (circuit)
|
|
85
|
+
circuit.probe = undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
releaseOwnersWithPrefix(prefix) {
|
|
89
|
+
for (const circuit of this.circuits.values()) {
|
|
90
|
+
if (circuit.probe?.owner.startsWith(prefix))
|
|
91
|
+
circuit.probe = undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
close(selector) {
|
|
95
|
+
this.circuits.delete(selector);
|
|
96
|
+
}
|
|
97
|
+
has(selector) {
|
|
98
|
+
return this.circuits.has(selector);
|
|
99
|
+
}
|
|
100
|
+
sweep(now) {
|
|
101
|
+
for (const [selector, circuit] of this.circuits) {
|
|
102
|
+
if (!circuit.probe && now >= circuit.openUntil + circuit.maxCooldownMs)
|
|
103
|
+
this.circuits.delete(selector);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export function createFallbackCircuitAccess(deps) {
|
|
108
|
+
const enabled = () => deps.settings().cooldownMs > 0;
|
|
109
|
+
return {
|
|
110
|
+
noteFailure(selector, failure) {
|
|
111
|
+
if (!enabled())
|
|
112
|
+
return;
|
|
113
|
+
const { cooldownMs, maxCooldownMs } = deps.settings();
|
|
114
|
+
const now = deps.now();
|
|
115
|
+
const retryAfterMs = failure.retryAfterMs ??
|
|
116
|
+
(failure.errorMessage === undefined ? undefined : parseRetryAfterMsMarker(failure.errorMessage));
|
|
117
|
+
deps.breaker.sweep(now);
|
|
118
|
+
const openUntil = deps.breaker.open(selector, { now, cooldownMs, maxCooldownMs, retryAfterMs });
|
|
119
|
+
deps.logger.info("circuit_opened", { selector, durationMs: openUntil - now, retryAfterMs });
|
|
120
|
+
},
|
|
121
|
+
governs: (selector) => enabled() && deps.breaker.has(selector),
|
|
122
|
+
isOpen: (selector, lane = "turn") => enabled() && deps.breaker.isOpen(selector, deps.now(), laneOwner(deps.owner(), lane)),
|
|
123
|
+
admit: (selector, lane = "turn") => enabled() ? deps.breaker.admit(selector, deps.now(), laneOwner(deps.owner(), lane)) : { kind: "closed" },
|
|
124
|
+
release: (token) => deps.breaker.release(token),
|
|
125
|
+
releaseAll: () => deps.breaker.releaseOwnersWithPrefix(`${deps.owner()}:`),
|
|
126
|
+
close: (selector) => deps.breaker.close(selector),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function laneOwner(session, lane) {
|
|
130
|
+
return `${session}:${lane}`;
|
|
131
|
+
}
|
|
132
|
+
const breakersByAgentDir = new Map();
|
|
133
|
+
/**
|
|
134
|
+
* An entry leaves the registry only when no live session holds it and it has no
|
|
135
|
+
* circuit left to share; a held breaker keeps its identity however empty it is.
|
|
136
|
+
*/
|
|
137
|
+
function evictUnheldEmptyBreakers() {
|
|
138
|
+
for (const [key, entry] of breakersByAgentDir) {
|
|
139
|
+
if (entry.owners === 0 && entry.breaker.size === 0)
|
|
140
|
+
breakersByAgentDir.delete(key);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function entryFor(agentDir) {
|
|
144
|
+
const key = resolve(agentDir);
|
|
145
|
+
let entry = breakersByAgentDir.get(key);
|
|
146
|
+
if (!entry) {
|
|
147
|
+
evictUnheldEmptyBreakers();
|
|
148
|
+
entry = { breaker: new FallbackCircuitBreaker(), owners: 0 };
|
|
149
|
+
breakersByAgentDir.set(key, entry);
|
|
150
|
+
}
|
|
151
|
+
return entry;
|
|
152
|
+
}
|
|
153
|
+
export function fallbackCircuitsFor(agentDir) {
|
|
154
|
+
return entryFor(agentDir).breaker;
|
|
155
|
+
}
|
|
156
|
+
/** Holds the agent dir's breaker for one session; `release` is idempotent. */
|
|
157
|
+
export function acquireFallbackCircuits(agentDir) {
|
|
158
|
+
const entry = entryFor(agentDir);
|
|
159
|
+
entry.owners++;
|
|
160
|
+
let released = false;
|
|
161
|
+
return {
|
|
162
|
+
breaker: entry.breaker,
|
|
163
|
+
release() {
|
|
164
|
+
if (released)
|
|
165
|
+
return;
|
|
166
|
+
released = true;
|
|
167
|
+
entry.owners--;
|
|
168
|
+
evictUnheldEmptyBreakers();
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
export function monotonicNow() {
|
|
173
|
+
return performance.timeOrigin + performance.now();
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=circuit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit.js","sourceRoot":"","sources":["../../../src/core/retry-fallback/circuit.ts"],"names":[],"mappings":"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","sourcesContent":["import { resolve } from \"node:path\";\nimport { parseRetryAfterMsMarker } from \"@earendil-works/pi-ai/utils/retry-hint\";\nimport type { FallbackLogger } from \"./log.ts\";\n\n/**\n * Fallback-chain circuit breaker. An entry that fails out of a configured chain\n * opens a circuit keyed by its base selector (`provider/id`); while it is open,\n * chain resolution skips the entry instead of spending its retry budget again.\n *\n * One breaker is shared by every session in the process that uses the same agent\n * directory - the config that defines the providers and selectors - so a session\n * started by `/new`, `/resume`, or `/fork` (each gets a fresh model runtime) and\n * an in-process subagent do not re-discover a dead provider.\n * The class is clock-free: every call receives `now`, keeping sessions' injected\n * clocks authoritative and tests deterministic.\n */\n\nexport const DEFAULT_CIRCUIT_COOLDOWN_MS = 60_000;\nexport const DEFAULT_CIRCUIT_MAX_COOLDOWN_MS = 30 * 60_000;\n\nexport interface FallbackCircuitSettings {\n\t/** First cooldown after an entry fails out of a chain; doubles per consecutive open. 0 disables the breaker. */\n\tcircuitCooldownMs?: number;\n\tcircuitMaxCooldownMs?: number;\n}\n\nexport interface ResolvedFallbackCircuitSettings {\n\tcooldownMs: number;\n\tmaxCooldownMs: number;\n}\n\nexport interface CircuitOpenRequest {\n\tnow: number;\n\tcooldownMs: number;\n\tmaxCooldownMs: number;\n\tretryAfterMs?: number;\n}\n\nexport interface ProbeToken {\n\treadonly selector: string;\n\treadonly owner: string;\n\treadonly generation: number;\n}\n\nexport type CircuitAdmission = { kind: \"closed\" } | { kind: \"open\" } | { kind: \"probe\"; token: ProbeToken };\n\ninterface SelectorCircuit {\n\topenUntil: number;\n\tretryFloorUntil: number;\n\tconsecutiveOpens: number;\n\tcooldownMs: number;\n\tmaxCooldownMs: number;\n\tprobe: { owner: string; generation: number } | undefined;\n}\n\nfunction nonNegativeMs(value: unknown, fallback: number): number {\n\treturn typeof value === \"number\" && Number.isFinite(value) && value >= 0 ? value : fallback;\n}\n\nexport function resolveFallbackCircuitSettings(\n\tsettings: FallbackCircuitSettings | undefined,\n): ResolvedFallbackCircuitSettings {\n\tconst cooldownMs = nonNegativeMs(settings?.circuitCooldownMs, DEFAULT_CIRCUIT_COOLDOWN_MS);\n\tconst maxCooldownMs = nonNegativeMs(settings?.circuitMaxCooldownMs, DEFAULT_CIRCUIT_MAX_COOLDOWN_MS);\n\treturn { cooldownMs, maxCooldownMs: Math.max(cooldownMs, maxCooldownMs) };\n}\n\nexport class FallbackCircuitBreaker {\n\tprivate readonly circuits = new Map<string, SelectorCircuit>();\n\tprivate generations = 0;\n\n\tget size(): number {\n\t\treturn this.circuits.size;\n\t}\n\n\topen(selector: string, request: CircuitOpenRequest): number {\n\t\tconst previous = this.circuits.get(selector);\n\t\tconst consecutiveOpens = (previous?.consecutiveOpens ?? 0) + 1;\n\t\tconst cooldownMs = Math.min(request.cooldownMs * 2 ** (consecutiveOpens - 1), request.maxCooldownMs);\n\t\tconst retryAfterMs = request.retryAfterMs;\n\t\tconst hinted =\n\t\t\tretryAfterMs !== undefined && Number.isFinite(retryAfterMs) && retryAfterMs > 0\n\t\t\t\t? request.now + retryAfterMs\n\t\t\t\t: 0;\n\t\tconst retryFloorUntil = Math.max(\n\t\t\thinted,\n\t\t\tprevious && previous.retryFloorUntil > request.now ? previous.retryFloorUntil : 0,\n\t\t);\n\t\tconst openUntil = Math.max(request.now + cooldownMs, retryFloorUntil);\n\t\tthis.circuits.set(selector, {\n\t\t\topenUntil,\n\t\t\tretryFloorUntil,\n\t\t\tconsecutiveOpens,\n\t\t\tcooldownMs,\n\t\t\tmaxCooldownMs: request.maxCooldownMs,\n\t\t\tprobe: undefined,\n\t\t});\n\t\treturn openUntil;\n\t}\n\n\tisOpen(selector: string, now: number, owner: string): boolean {\n\t\tconst circuit = this.circuits.get(selector);\n\t\tif (!circuit) return false;\n\t\tif (now < circuit.openUntil) return true;\n\t\treturn circuit.probe !== undefined && circuit.probe.owner !== owner;\n\t}\n\n\t/**\n\t * Atomic admission: a closed selector passes, a cooling one or one whose probe\n\t * another requester holds is refused, and a half-open one hands `owner` its\n\t * single probe under a fresh generation. The probe stays exclusive until its\n\t * token is released or the circuit settles - it never expires on a timer, so a\n\t * slow probe cannot overlap a second one, and a stale token never releases a\n\t * later acquisition.\n\t */\n\tadmit(selector: string, now: number, owner: string): CircuitAdmission {\n\t\tconst circuit = this.circuits.get(selector);\n\t\tif (!circuit) return { kind: \"closed\" };\n\t\tif (now < circuit.openUntil) return { kind: \"open\" };\n\t\tif (circuit.probe && circuit.probe.owner !== owner) return { kind: \"open\" };\n\t\tcircuit.probe ??= { owner, generation: ++this.generations };\n\t\treturn { kind: \"probe\", token: { selector, owner, generation: circuit.probe.generation } };\n\t}\n\n\trelease(token: ProbeToken): void {\n\t\tconst probe = this.circuits.get(token.selector)?.probe;\n\t\tif (probe && probe.owner === token.owner && probe.generation === token.generation) {\n\t\t\tconst circuit = this.circuits.get(token.selector);\n\t\t\tif (circuit) circuit.probe = undefined;\n\t\t}\n\t}\n\n\treleaseOwnersWithPrefix(prefix: string): void {\n\t\tfor (const circuit of this.circuits.values()) {\n\t\t\tif (circuit.probe?.owner.startsWith(prefix)) circuit.probe = undefined;\n\t\t}\n\t}\n\n\tclose(selector: string): void {\n\t\tthis.circuits.delete(selector);\n\t}\n\n\thas(selector: string): boolean {\n\t\treturn this.circuits.has(selector);\n\t}\n\n\tsweep(now: number): void {\n\t\tfor (const [selector, circuit] of this.circuits) {\n\t\t\tif (!circuit.probe && now >= circuit.openUntil + circuit.maxCooldownMs) this.circuits.delete(selector);\n\t\t}\n\t}\n}\n\n/**\n * A session's requests are admitted per lane, not per session: the foreground\n * turn is one lane, each background probe-back its own. Two lanes of one session\n * never share a probe, so a probe-back in flight keeps the foreground turn off\n * the entry exactly as it keeps a sibling session off.\n */\nexport type CircuitLane = \"turn\" | `probe-back:${number}`;\n\nexport interface FallbackCircuitAccess {\n\tnoteFailure(selector: string, failure: { retryAfterMs?: number; errorMessage?: string }): void;\n\t/** Whether the breaker holds any state for the selector, i.e. it, not a session cooldown, governs recovery. */\n\tgoverns(selector: string): boolean;\n\tisOpen(selector: string, lane?: CircuitLane): boolean;\n\tadmit(selector: string, lane?: CircuitLane): CircuitAdmission;\n\trelease(token: ProbeToken): void;\n\treleaseAll(): void;\n\tclose(selector: string): void;\n}\n\nexport interface FallbackCircuitAccessDeps {\n\tbreaker: FallbackCircuitBreaker;\n\towner(): string;\n\tnow(): number;\n\tsettings(): ResolvedFallbackCircuitSettings;\n\tlogger: FallbackLogger;\n}\n\nexport function createFallbackCircuitAccess(deps: FallbackCircuitAccessDeps): FallbackCircuitAccess {\n\tconst enabled = () => deps.settings().cooldownMs > 0;\n\treturn {\n\t\tnoteFailure(selector, failure) {\n\t\t\tif (!enabled()) return;\n\t\t\tconst { cooldownMs, maxCooldownMs } = deps.settings();\n\t\t\tconst now = deps.now();\n\t\t\tconst retryAfterMs =\n\t\t\t\tfailure.retryAfterMs ??\n\t\t\t\t(failure.errorMessage === undefined ? undefined : parseRetryAfterMsMarker(failure.errorMessage));\n\t\t\tdeps.breaker.sweep(now);\n\t\t\tconst openUntil = deps.breaker.open(selector, { now, cooldownMs, maxCooldownMs, retryAfterMs });\n\t\t\tdeps.logger.info(\"circuit_opened\", { selector, durationMs: openUntil - now, retryAfterMs });\n\t\t},\n\t\tgoverns: (selector) => enabled() && deps.breaker.has(selector),\n\t\tisOpen: (selector, lane = \"turn\") =>\n\t\t\tenabled() && deps.breaker.isOpen(selector, deps.now(), laneOwner(deps.owner(), lane)),\n\t\tadmit: (selector, lane = \"turn\") =>\n\t\t\tenabled() ? deps.breaker.admit(selector, deps.now(), laneOwner(deps.owner(), lane)) : { kind: \"closed\" },\n\t\trelease: (token) => deps.breaker.release(token),\n\t\treleaseAll: () => deps.breaker.releaseOwnersWithPrefix(`${deps.owner()}:`),\n\t\tclose: (selector) => deps.breaker.close(selector),\n\t};\n}\n\nfunction laneOwner(session: string, lane: CircuitLane): string {\n\treturn `${session}:${lane}`;\n}\n\ninterface BreakerEntry {\n\treadonly breaker: FallbackCircuitBreaker;\n\towners: number;\n}\n\nconst breakersByAgentDir = new Map<string, BreakerEntry>();\n\n/**\n * An entry leaves the registry only when no live session holds it and it has no\n * circuit left to share; a held breaker keeps its identity however empty it is.\n */\nfunction evictUnheldEmptyBreakers(): void {\n\tfor (const [key, entry] of breakersByAgentDir) {\n\t\tif (entry.owners === 0 && entry.breaker.size === 0) breakersByAgentDir.delete(key);\n\t}\n}\n\nfunction entryFor(agentDir: string): BreakerEntry {\n\tconst key = resolve(agentDir);\n\tlet entry = breakersByAgentDir.get(key);\n\tif (!entry) {\n\t\tevictUnheldEmptyBreakers();\n\t\tentry = { breaker: new FallbackCircuitBreaker(), owners: 0 };\n\t\tbreakersByAgentDir.set(key, entry);\n\t}\n\treturn entry;\n}\n\nexport function fallbackCircuitsFor(agentDir: string): FallbackCircuitBreaker {\n\treturn entryFor(agentDir).breaker;\n}\n\n/** Holds the agent dir's breaker for one session; `release` is idempotent. */\nexport function acquireFallbackCircuits(agentDir: string): { breaker: FallbackCircuitBreaker; release(): void } {\n\tconst entry = entryFor(agentDir);\n\tentry.owners++;\n\tlet released = false;\n\treturn {\n\t\tbreaker: entry.breaker,\n\t\trelease() {\n\t\t\tif (released) return;\n\t\t\treleased = true;\n\t\t\tentry.owners--;\n\t\t\tevictUnheldEmptyBreakers();\n\t\t},\n\t};\n}\n\nexport function monotonicNow(): number {\n\treturn performance.timeOrigin + performance.now();\n}\n"]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { FallbackCircuitAccess } from "./circuit.ts";
|
|
4
|
+
import type { SelectorCooldowns } from "./cooldown.ts";
|
|
5
|
+
import type { FallbackLogger } from "./log.ts";
|
|
6
|
+
export interface ActiveFallbackState {
|
|
7
|
+
chainKey: string;
|
|
8
|
+
originalSelector: string;
|
|
9
|
+
originalThinkingLevel?: ThinkingLevel;
|
|
10
|
+
lastAppliedThinkingLevel?: ThinkingLevel;
|
|
11
|
+
/** Pin provenance: refusal contributions release on compaction, billing never. */
|
|
12
|
+
pinnedByRefusal: boolean;
|
|
13
|
+
pinnedByBilling: boolean;
|
|
14
|
+
/** Derived OR of the two provenance flags - the only field consumers read. */
|
|
15
|
+
pinned: boolean;
|
|
16
|
+
}
|
|
17
|
+
export type FallbackReason = "transient" | "refusal" | "hard-error" | "billing";
|
|
18
|
+
export type CircuitFailure = {
|
|
19
|
+
errorMessage?: string;
|
|
20
|
+
retryAfterMs?: number;
|
|
21
|
+
};
|
|
22
|
+
export interface FallbackSettings {
|
|
23
|
+
modelFallback: boolean;
|
|
24
|
+
chains: Readonly<Record<string, readonly string[]>>;
|
|
25
|
+
}
|
|
26
|
+
export interface RetryFallbackControllerDeps {
|
|
27
|
+
getSettings(): FallbackSettings;
|
|
28
|
+
registry: {
|
|
29
|
+
find(provider: string, id: string): Model<Api> | undefined;
|
|
30
|
+
getAll(): Model<Api>[];
|
|
31
|
+
/** Ranks bare-selector expansion: OAuth-credential providers come first. */
|
|
32
|
+
isUsingOAuth?(model: Model<Api>): boolean;
|
|
33
|
+
/** Filters bare-selector expansion: a definitive `false` keeps a lane that can never serve out of the chain. */
|
|
34
|
+
isFallbackEligible?(model: Model<Api>): boolean;
|
|
35
|
+
};
|
|
36
|
+
cooldowns: SelectorCooldowns;
|
|
37
|
+
/** Chain-entry circuits shared with every session in the process on the same agent dir. */
|
|
38
|
+
circuits?: FallbackCircuitAccess;
|
|
39
|
+
logger: FallbackLogger;
|
|
40
|
+
switchModel(model: Model<Api>, thinking: ThinkingLevel, reason: "fallback" | "fallback-revert"): Promise<void>;
|
|
41
|
+
emit(event: {
|
|
42
|
+
type: "retry_fallback_applied";
|
|
43
|
+
from: string;
|
|
44
|
+
to: string;
|
|
45
|
+
chainKey: string;
|
|
46
|
+
reason: FallbackReason;
|
|
47
|
+
} | {
|
|
48
|
+
type: "retry_fallback_reverted";
|
|
49
|
+
from: string;
|
|
50
|
+
to: string;
|
|
51
|
+
}): void;
|
|
52
|
+
getCurrentSelector(): {
|
|
53
|
+
model: Model<Api>;
|
|
54
|
+
thinkingLevel?: ThinkingLevel;
|
|
55
|
+
} | undefined;
|
|
56
|
+
isAuthAvailable(provider: string): boolean;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=controller-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-types.d.ts","sourceRoot":"","sources":["../../../src/core/retry-fallback/controller-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C,MAAM,WAAW,mBAAmB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,qBAAqB,CAAC,EAAE,aAAa,CAAC;IACtC,wBAAwB,CAAC,EAAE,aAAa,CAAC;IACzC,kFAAkF;IAClF,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,EAAE,OAAO,CAAC;IACzB,8EAA8E;IAC9E,MAAM,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,CAAC;AAChF,MAAM,MAAM,cAAc,GAAG;IAAE,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9E,MAAM,WAAW,gBAAgB;IAChC,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;CACpD;AAED,MAAM,WAAW,2BAA2B;IAC3C,WAAW,IAAI,gBAAgB,CAAC;IAChC,QAAQ,EAAE;QACT,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;QAC3D,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACvB,4EAA4E;QAC5E,YAAY,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;QAC1C,gHAAgH;QAChH,kBAAkB,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;KAChD,CAAC;IACF,SAAS,EAAE,iBAAiB,CAAC;IAC7B,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,qBAAqB,CAAC;IACjC,MAAM,EAAE,cAAc,CAAC;IACvB,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,UAAU,GAAG,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/G,IAAI,CACH,KAAK,EACF;QACA,IAAI,EAAE,wBAAwB,CAAC;QAC/B,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,cAAc,CAAC;KACtB,GACD;QAAE,IAAI,EAAE,yBAAyB,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAC9D,IAAI,CAAC;IACR,kBAAkB,IAAI;QAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QAAC,aAAa,CAAC,EAAE,aAAa,CAAA;KAAE,GAAG,SAAS,CAAC;IACvF,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;CAC3C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-types.js","sourceRoot":"","sources":["../../../src/core/retry-fallback/controller-types.ts"],"names":[],"mappings":"","sourcesContent":["import type { ThinkingLevel } from \"@earendil-works/pi-agent-core\";\nimport type { Api, Model } from \"@earendil-works/pi-ai\";\nimport type { FallbackCircuitAccess } from \"./circuit.ts\";\nimport type { SelectorCooldowns } from \"./cooldown.ts\";\nimport type { FallbackLogger } from \"./log.ts\";\n\nexport interface ActiveFallbackState {\n\tchainKey: string;\n\toriginalSelector: string;\n\toriginalThinkingLevel?: ThinkingLevel;\n\tlastAppliedThinkingLevel?: ThinkingLevel;\n\t/** Pin provenance: refusal contributions release on compaction, billing never. */\n\tpinnedByRefusal: boolean;\n\tpinnedByBilling: boolean;\n\t/** Derived OR of the two provenance flags - the only field consumers read. */\n\tpinned: boolean;\n}\n\nexport type FallbackReason = \"transient\" | \"refusal\" | \"hard-error\" | \"billing\";\nexport type CircuitFailure = { errorMessage?: string; retryAfterMs?: number };\n\nexport interface FallbackSettings {\n\tmodelFallback: boolean;\n\tchains: Readonly<Record<string, readonly string[]>>;\n}\n\nexport interface RetryFallbackControllerDeps {\n\tgetSettings(): FallbackSettings;\n\tregistry: {\n\t\tfind(provider: string, id: string): Model<Api> | undefined;\n\t\tgetAll(): Model<Api>[];\n\t\t/** Ranks bare-selector expansion: OAuth-credential providers come first. */\n\t\tisUsingOAuth?(model: Model<Api>): boolean;\n\t\t/** Filters bare-selector expansion: a definitive `false` keeps a lane that can never serve out of the chain. */\n\t\tisFallbackEligible?(model: Model<Api>): boolean;\n\t};\n\tcooldowns: SelectorCooldowns;\n\t/** Chain-entry circuits shared with every session in the process on the same agent dir. */\n\tcircuits?: FallbackCircuitAccess;\n\tlogger: FallbackLogger;\n\tswitchModel(model: Model<Api>, thinking: ThinkingLevel, reason: \"fallback\" | \"fallback-revert\"): Promise<void>;\n\temit(\n\t\tevent:\n\t\t\t| {\n\t\t\t\t\ttype: \"retry_fallback_applied\";\n\t\t\t\t\tfrom: string;\n\t\t\t\t\tto: string;\n\t\t\t\t\tchainKey: string;\n\t\t\t\t\treason: FallbackReason;\n\t\t\t }\n\t\t\t| { type: \"retry_fallback_reverted\"; from: string; to: string },\n\t): void;\n\tgetCurrentSelector(): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined;\n\tisAuthAvailable(provider: string): boolean;\n}\n"]}
|