@code-yeongyu/senpi 2026.11.3-1 → 2026.11.3-2
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 +27 -0
- package/dist/bundle/chunks/anthropic-messages-LJAKRKMX.js +11 -0
- package/dist/bundle/chunks/{args-O3WS4WCO.js → args-GQTXDYS3.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-YLBBQXI7.js → azure-openai-responses-HDEKTPMZ.js} +1 -1
- package/dist/bundle/chunks/chatgpt-subscription.js +10 -2
- package/dist/bundle/chunks/{chunk-ED7VJGL4.js → chunk-2PGSRQBF.js} +1 -1
- package/dist/bundle/chunks/{chunk-4BF4FLT6.js → chunk-3LDASCJN.js} +1 -1
- package/dist/bundle/chunks/{chunk-PIAY636U.js → chunk-3QPE75AR.js} +1 -1
- package/dist/bundle/chunks/{chunk-FQZZGSYH.js → chunk-4ZGQUOBV.js} +1 -1
- package/dist/bundle/chunks/{chunk-S2M5D5GS.js → chunk-5BMIET5O.js} +2 -2
- package/dist/bundle/chunks/{chunk-XLLQJALL.js → chunk-67M7HKXO.js} +1 -1
- package/dist/bundle/chunks/chunk-6ISOXWH2.js +2 -0
- package/dist/bundle/chunks/{chunk-7KLEFHSU.js → chunk-6NPK2O3C.js} +2 -2
- package/dist/bundle/chunks/{chunk-BNIZP4OS.js → chunk-7C4W5HMU.js} +1 -1
- package/dist/bundle/chunks/chunk-7KIHKE2T.js +2 -0
- package/dist/bundle/chunks/{chunk-WCRNJYHO.js → chunk-A4KOQUHT.js} +1 -1
- package/dist/bundle/chunks/{chunk-TOOB6OLG.js → chunk-A5PZGQCP.js} +1 -1
- package/dist/bundle/chunks/{chunk-TBARMJ75.js → chunk-AVP6DDBF.js} +1 -1
- package/dist/bundle/chunks/{chunk-FLM6GZPT.js → chunk-B3GTID4P.js} +1 -1
- package/dist/bundle/chunks/{chunk-2XBKE2BZ.js → chunk-BIWFWGUA.js} +1 -1
- package/dist/bundle/chunks/{chunk-6LDUM5CL.js → chunk-DCI4QOUX.js} +1 -1
- package/dist/bundle/chunks/{chunk-2YFPDNNP.js → chunk-DNZKKG4V.js} +1 -1
- package/dist/bundle/chunks/{chunk-W2V7RIBF.js → chunk-E7SA75PP.js} +1 -1
- package/dist/bundle/chunks/{chunk-YWD54RHU.js → chunk-EBZ6IP4V.js} +1 -1
- package/dist/bundle/chunks/{chunk-J2OBN7TI.js → chunk-G5XUGK4B.js} +1 -1
- package/dist/bundle/chunks/{chunk-PX7FF5LE.js → chunk-GKP34H3C.js} +2 -2
- package/dist/bundle/chunks/{chunk-PP52OWBB.js → chunk-GLTOBXZY.js} +1 -1
- package/dist/bundle/chunks/{chunk-ZNLCKKF6.js → chunk-IJGXAACA.js} +117 -35
- package/dist/bundle/chunks/{chunk-LGO25HPL.js → chunk-JCTODLUP.js} +1 -1
- package/dist/bundle/chunks/{chunk-3ESZE3DG.js → chunk-KHBCZ6TR.js} +1 -1
- package/dist/bundle/chunks/chunk-KJR3XXMI.js +2 -0
- package/dist/bundle/chunks/{chunk-FJ2UCBOA.js → chunk-KY4V3TSO.js} +2 -2
- package/dist/bundle/chunks/{chunk-4SENPCEA.js → chunk-MCXOJHN2.js} +1 -1
- package/dist/bundle/chunks/{chunk-DPBPO5OU.js → chunk-NKI7L5UM.js} +3 -3
- package/dist/bundle/chunks/{chunk-CJWSQ6CK.js → chunk-OD7RT7G7.js} +1 -1
- package/dist/bundle/chunks/{chunk-F4OHTL2K.js → chunk-OOU3BRUY.js} +1 -1
- package/dist/bundle/chunks/{chunk-LPHUMR6R.js → chunk-OT6ALXSJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-XGBRBCIQ.js → chunk-Q5CKFBQT.js} +1 -1
- package/dist/bundle/chunks/{chunk-F7GBSQRV.js → chunk-QPYDQ4QK.js} +1 -1
- package/dist/bundle/chunks/{chunk-MOOFLKLI.js → chunk-QSMT5KVJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-SAZ6E74O.js → chunk-RUOTFZQR.js} +1 -1
- package/dist/bundle/chunks/{chunk-5DZD623T.js → chunk-RWF6IJQL.js} +2 -2
- package/dist/bundle/chunks/{chunk-HBCNDC53.js → chunk-S5Y7NEHB.js} +1 -1
- package/dist/bundle/chunks/{chunk-DAPBNRN5.js → chunk-SCBTASM4.js} +1 -1
- package/dist/bundle/chunks/chunk-SJWTZ3Z2.js +2 -0
- package/dist/bundle/chunks/{chunk-ISZ4EWIS.js → chunk-SODMQEHO.js} +1 -1
- package/dist/bundle/chunks/{chunk-WEGTCUO3.js → chunk-SQAQEITV.js} +1 -1
- package/dist/bundle/chunks/{chunk-UFVZTQKN.js → chunk-TQQG5L4D.js} +1 -1
- package/dist/bundle/chunks/{chunk-VPDQ6MOL.js → chunk-U6A4X3FV.js} +1 -1
- package/dist/bundle/chunks/{chunk-ACIFXWJ6.js → chunk-UHUM55TR.js} +1 -1
- package/dist/bundle/chunks/{chunk-L5OFGHDA.js → chunk-USKWGPA6.js} +1 -1
- package/dist/bundle/chunks/{chunk-7ORS33CE.js → chunk-VDDJADPV.js} +1 -1
- package/dist/bundle/chunks/{chunk-R65P6LAT.js → chunk-VIHXG53S.js} +1 -1
- package/dist/bundle/chunks/{chunk-SGS76663.js → chunk-VPVD5G32.js} +1 -1
- package/dist/bundle/chunks/chunk-WO6I5PRF.js +3 -0
- package/dist/bundle/chunks/{chunk-ETCYVVVO.js → chunk-WVEIJ437.js} +1 -1
- package/dist/bundle/chunks/{chunk-7565XQAU.js → chunk-XKFG4N3Z.js} +1 -1
- package/dist/bundle/chunks/{chunk-57MRFI6E.js → chunk-XMWPCJU6.js} +1 -1
- package/dist/bundle/chunks/{chunk-GDPWQJDQ.js → chunk-XPSXUKSD.js} +1 -1
- package/dist/bundle/chunks/{chunk-FNBEICAA.js → chunk-YI6KRJDC.js} +1 -1
- package/dist/bundle/chunks/{chunk-UURGXV5B.js → chunk-YP47G4YP.js} +1 -1
- package/dist/bundle/chunks/{chunk-6BSMXZKQ.js → chunk-YRYDCKUI.js} +1 -1
- package/dist/bundle/chunks/{chunk-QTQ3ES2S.js → chunk-YTKZSGMQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-V36CXIIB.js → chunk-YXL3PNTL.js} +1 -1
- package/dist/bundle/chunks/chunk-Z7HTHT6K.js +2 -0
- package/dist/bundle/chunks/{chunk-27XEDYQA.js → chunk-ZTPG27EJ.js} +1 -1
- package/dist/bundle/chunks/{cli-main-XM5FWY3Z.js → cli-main-RN7YB427.js} +1 -1
- package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-FC7QLRU2.js → cloudflare-workers-ai-system-one-N5KFNKK4.js} +1 -1
- package/dist/bundle/chunks/credential-accounts-BJX55MTT.js +2 -0
- package/dist/bundle/chunks/devin.js +10 -2
- package/dist/bundle/chunks/{dispatch-UY6LBJJE.js → dispatch-M7UAPGVY.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-7PQ4QRHJ.js → google-generative-ai-2TZSCSIS.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-DY7EO4EJ.js → google-vertex-7NJJOI7V.js} +1 -1
- package/dist/bundle/chunks/{health-write-45CX64T2.js → health-write-YUSPVH7U.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-3EDGRKQM.js → help-fast-path-DWCA77QP.js} +1 -1
- package/dist/bundle/chunks/host-command-KJXUTSQ7.js +2 -0
- package/dist/bundle/chunks/{host-lifecycle-TGG7KGQH.js → host-lifecycle-QHIJ4G2K.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +1 -1
- package/dist/bundle/chunks/{host-session-command-OJFARE5Q.js → host-session-command-IPH3WXSG.js} +1 -1
- package/dist/bundle/chunks/{host-socket-request-F2R5P34M.js → host-socket-request-XPXJV3CS.js} +1 -1
- package/dist/bundle/chunks/interactive-mode-XUXMDYLA.js +2 -0
- package/dist/bundle/chunks/{list-tips-3D3A6GFK.js → list-tips-XJY7EL5S.js} +1 -1
- package/dist/bundle/chunks/{login-health-YOPTYRTU.js → login-health-226NO44U.js} +1 -1
- package/dist/bundle/chunks/main-P7PKMJUG.js +2 -0
- package/dist/bundle/chunks/{mistral-conversations-6F2RAIB2.js → mistral-conversations-QMHMHSUF.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-N2RQ7JSZ.js → multi-session-host-ZQY4AN46.js} +1 -1
- package/dist/bundle/chunks/{native-loader-NMDTMHMA.js → native-loader-A6DNMGCK.js} +1 -1
- package/dist/bundle/chunks/openai-chatgpt.js +11 -3
- package/dist/bundle/chunks/{openai-codex-responses-YTWLG4OL.js → openai-codex-responses-GUAHKQE2.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-TIFDYHZL.js → openai-completions-UPRTFM54.js} +1 -1
- package/dist/bundle/chunks/{openai-images-OAEMY4RG.js → openai-images-IKWNS6VB.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-L35NIG5Y.js → openai-responses-AXN5NUHH.js} +1 -1
- package/dist/bundle/chunks/openrouter.js +10 -2
- package/dist/bundle/chunks/{package-manager-cli-JPNA27E5.js → package-manager-cli-PP7L2Q5M.js} +1 -1
- package/dist/bundle/chunks/radius.js +10 -2
- package/dist/bundle/chunks/{rg-engine-JDUYBM6L.js → rg-engine-VRSXT6HG.js} +1 -1
- package/dist/bundle/chunks/{role-45Y7PQFC.js → role-L5J7ZPCF.js} +1 -1
- package/dist/bundle/chunks/rotation-stream-6INZXQFC.js +2 -0
- package/dist/bundle/chunks/rpc-mode-W4UATRAT.js +2 -0
- package/dist/bundle/chunks/{rpc-monitor-source-5TUR27NY.js → rpc-monitor-source-RMMUPBMS.js} +1 -1
- package/dist/bundle/chunks/{run-attached-tui-TI6MELEI.js → run-attached-tui-RLXFGCEP.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-OZ5ESMIT.js → schedule-command-ZNFEJNTT.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-5YKBYJOO.js → session-control-endpoint-TWTONJWF.js} +1 -1
- package/dist/bundle/chunks/{session-picker-CMRV72VA.js → session-picker-KRUVCHOV.js} +1 -1
- package/dist/bundle/chunks/session-slot-NJOZCOHQ.js +2 -0
- package/dist/bundle/chunks/session-worker.js +218 -135
- package/dist/bundle/chunks/{state-store-Q47AYMRI.js → state-store-ZAFZ4JOM.js} +1 -1
- package/dist/bundle/chunks/{typesafe-system-one-5JYDJK6S.js → typesafe-system-one-CICIRAYN.js} +1 -1
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/host-supervisor.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session.js +5 -3
- package/dist/core/auth-storage.d.ts +19 -0
- package/dist/core/auth-storage.js +99 -24
- package/dist/core/auth-store-load-error.d.ts +29 -0
- package/dist/core/auth-store-load-error.js +74 -0
- package/dist/core/brand.d.ts +2 -0
- package/dist/core/brand.js +1 -0
- package/dist/core/credential-pool/failover.d.ts +8 -0
- package/dist/core/credential-pool/failover.js +18 -5
- package/dist/core/credential-pool/rotation-stream.d.ts +1 -1
- package/dist/core/credential-pool/rotation-stream.js +2 -7
- package/dist/core/credential-pool/session-slot.d.ts +20 -0
- package/dist/core/credential-pool/session-slot.js +26 -0
- package/dist/core/credential-recovery-revision.d.ts +10 -0
- package/dist/core/credential-recovery-revision.js +19 -0
- package/dist/core/execution/executor-body.d.ts +22 -0
- package/dist/core/execution/executor-body.js +73 -0
- package/dist/core/execution/executor-freeze.d.ts +17 -0
- package/dist/core/execution/executor-freeze.js +53 -0
- package/dist/core/execution/executor-pipeline.d.ts +9 -0
- package/dist/core/execution/executor-pipeline.js +25 -0
- package/dist/core/execution/executor-types.d.ts +82 -0
- package/dist/core/execution/executor-types.js +2 -0
- package/dist/core/execution/executor.d.ts +8 -0
- package/dist/core/execution/executor.js +216 -0
- package/dist/core/extensions/builtin/anthropic-subscription/account-block-log.d.ts +15 -0
- package/dist/core/extensions/builtin/anthropic-subscription/account-block-log.js +18 -0
- package/dist/core/extensions/builtin/anthropic-subscription/affinity.d.ts +9 -1
- package/dist/core/extensions/builtin/anthropic-subscription/affinity.js +15 -4
- package/dist/core/extensions/builtin/anthropic-subscription/failover.d.ts +7 -1
- package/dist/core/extensions/builtin/anthropic-subscription/failover.js +19 -2
- package/dist/core/extensions/builtin/anthropic-subscription/guidance.d.ts +8 -2
- package/dist/core/extensions/builtin/anthropic-subscription/guidance.js +25 -1
- package/dist/core/extensions/builtin/anthropic-subscription/provider-diagnostic.d.ts +9 -0
- package/dist/core/extensions/builtin/anthropic-subscription/provider-diagnostic.js +59 -0
- package/dist/core/extensions/builtin/anthropic-subscription/session-registry-wiring.js +7 -0
- package/dist/core/extensions/builtin/anthropic-subscription/stream-guidance.js +5 -3
- package/dist/core/extensions/builtin/anthropic-subscription/stream.js +4 -0
- package/dist/core/extensions/builtin/btw/index.js +3 -1
- package/dist/core/extensions/builtin/cache-keepalive/index.js +3 -1
- package/dist/core/extensions/builtin/compaction/index.js +1 -1
- package/dist/core/extensions/builtin/compaction/openai-remote.d.ts +3 -1
- package/dist/core/extensions/builtin/compaction/openai-remote.js +1 -1
- package/dist/core/extensions/builtin/compaction/speculative.js +3 -2
- package/dist/core/extensions/builtin/cursor-cli-oauth/account-command.js +3 -1
- package/dist/core/extensions/builtin/cursor-cli-oauth/native-bootstrap.js +3 -1
- package/dist/core/extensions/builtin/look-at/runner.js +3 -1
- package/dist/core/extensions/builtin/mcp/auth/callback.js +3 -4
- package/dist/core/extensions/builtin/mcp/auth/oauth-provider.js +2 -1
- package/dist/core/model-registry.d.ts +10 -2
- package/dist/core/model-registry.js +12 -4
- package/dist/core/model-runtime.d.ts +8 -0
- package/dist/core/model-runtime.js +38 -0
- package/dist/core/retry-fallback/controller-types.d.ts +7 -0
- package/dist/core/retry-fallback/controller.d.ts +2 -0
- package/dist/core/retry-fallback/controller.js +13 -4
- package/dist/core/retry-fallback/credential-recovery.d.ts +18 -0
- package/dist/core/retry-fallback/credential-recovery.js +32 -0
- package/dist/core/retry-fallback/settings.d.ts +8 -1
- package/dist/core/retry-fallback/settings.js +12 -6
- package/dist/core/session-log.js +1 -1
- package/dist/core/settings-manager.js +4 -1
- package/dist/modes/rpc/connection-handler.js +3 -1
- package/dist/modes/rpc/effect-commands.d.ts +1 -0
- package/dist/modes/rpc/loopless-execute-grant.d.ts +20 -0
- package/dist/modes/rpc/loopless-execute-grant.js +26 -0
- package/dist/modes/rpc/loopless-runtime-settings.d.ts +30 -0
- package/dist/modes/rpc/loopless-runtime-settings.js +69 -0
- package/dist/modes/rpc/loopless-tool-call.d.ts +12 -0
- package/dist/modes/rpc/loopless-tool-call.js +22 -0
- package/dist/modes/rpc/rpc-read-response-types.d.ts +2 -1
- package/dist/modes/rpc/rpc-session-state-types.d.ts +10 -0
- package/dist/modes/rpc/rpc-types.d.ts +1 -1
- package/dist/modes/rpc/runtime-factory-loopless.d.ts +57 -0
- package/dist/modes/rpc/runtime-factory-loopless.js +131 -0
- package/docs/development.md +1 -1
- package/docs/rpc.md +2 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/anthropic-messages-AFWQB3KN.js +0 -10
- package/dist/bundle/chunks/chunk-3WJ2DE2G.js +0 -3
- package/dist/bundle/chunks/chunk-ESQBKTNG.js +0 -2
- package/dist/bundle/chunks/chunk-MOH5HOHX.js +0 -2
- package/dist/bundle/chunks/credential-accounts-YZYSKA7X.js +0 -2
- package/dist/bundle/chunks/host-command-PZ4VYQIW.js +0 -2
- package/dist/bundle/chunks/interactive-mode-7JSJVVIS.js +0 -2
- package/dist/bundle/chunks/main-3YGJ2O4I.js +0 -2
- package/dist/bundle/chunks/rotation-stream-WXWJKJZN.js +0 -2
- package/dist/bundle/chunks/rpc-mode-Z7MN2ZWA.js +0 -2
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { FrozenCall, StoreValue } from "@earendil-works/pi-agent-core/conformance";
|
|
3
|
+
import type { PrepareReply } from "./executor-types.ts";
|
|
4
|
+
/** Tool arguments and results are JSON on every model and wire path; a round trip drops what JSON cannot carry. */
|
|
5
|
+
export declare function toStoreValue(value: unknown): StoreValue;
|
|
6
|
+
export declare function digestOf(value: StoreValue): string;
|
|
7
|
+
/** The call after the whole `tool_call` chain, detached from every object a handler or caller still holds. */
|
|
8
|
+
export declare function freezeCall(name: string, args: unknown): FrozenCall;
|
|
9
|
+
/** The durable record of a settled body: what the issuer commits before ACK. Details stay in the in-process reply. */
|
|
10
|
+
export declare function resultValue(result: AgentToolResult<unknown>, isError: boolean, requestedName?: string): StoreValue;
|
|
11
|
+
export type StoppedReply = Extract<PrepareReply, {
|
|
12
|
+
readonly kind: "blocked" | "batch_abort";
|
|
13
|
+
}>;
|
|
14
|
+
/** A preparation that ended without a frozen call, as the ledger's durable `blocked` outcome value. */
|
|
15
|
+
export declare function stoppedValue(reply: StoppedReply): StoreValue;
|
|
16
|
+
export declare function stoppedReply(value: StoreValue): StoppedReply | undefined;
|
|
17
|
+
//# sourceMappingURL=executor-freeze.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { errorResult } from "./executor-body.js";
|
|
3
|
+
function canonical(value) {
|
|
4
|
+
if (value === null || typeof value !== "object")
|
|
5
|
+
return JSON.stringify(value);
|
|
6
|
+
if (Array.isArray(value))
|
|
7
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
8
|
+
const entries = Object.entries(value).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
|
|
9
|
+
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonical(entry)}`).join(",")}}`;
|
|
10
|
+
}
|
|
11
|
+
/** Tool arguments and results are JSON on every model and wire path; a round trip drops what JSON cannot carry. */
|
|
12
|
+
export function toStoreValue(value) {
|
|
13
|
+
const json = JSON.stringify(value);
|
|
14
|
+
return json === undefined ? null : JSON.parse(json);
|
|
15
|
+
}
|
|
16
|
+
export function digestOf(value) {
|
|
17
|
+
return createHash("sha256").update(canonical(value)).digest("hex");
|
|
18
|
+
}
|
|
19
|
+
/** The call after the whole `tool_call` chain, detached from every object a handler or caller still holds. */
|
|
20
|
+
export function freezeCall(name, args) {
|
|
21
|
+
const frozenArgs = toStoreValue(args);
|
|
22
|
+
return { name, args: frozenArgs, digest: digestOf({ name, args: frozenArgs }) };
|
|
23
|
+
}
|
|
24
|
+
/** The durable record of a settled body: what the issuer commits before ACK. Details stay in the in-process reply. */
|
|
25
|
+
export function resultValue(result, isError, requestedName) {
|
|
26
|
+
return toStoreValue({ content: result.content ?? [], isError, requestedName });
|
|
27
|
+
}
|
|
28
|
+
/** A preparation that ended without a frozen call, as the ledger's durable `blocked` outcome value. */
|
|
29
|
+
export function stoppedValue(reply) {
|
|
30
|
+
const text = (reply.result.content ?? []).map((part) => (part.type === "text" ? part.text : "")).join("");
|
|
31
|
+
const requestedName = reply.kind === "blocked" ? reply.requestedName : undefined;
|
|
32
|
+
return toStoreValue({ kind: reply.kind, text, terminate: reply.result.terminate === true, requestedName });
|
|
33
|
+
}
|
|
34
|
+
/** The reply a re-sent prepare gets back from a durable `blocked` outcome; undefined for a value this module never wrote. */
|
|
35
|
+
function isRecord(value) {
|
|
36
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
export function stoppedReply(value) {
|
|
39
|
+
if (!isRecord(value))
|
|
40
|
+
return undefined;
|
|
41
|
+
const { kind, text, terminate, requestedName } = value;
|
|
42
|
+
if (typeof text !== "string" || typeof terminate !== "boolean")
|
|
43
|
+
return undefined;
|
|
44
|
+
const result = errorResult(text);
|
|
45
|
+
if (terminate)
|
|
46
|
+
result.terminate = true;
|
|
47
|
+
if (kind === "batch_abort")
|
|
48
|
+
return { kind, result };
|
|
49
|
+
if (kind !== "blocked")
|
|
50
|
+
return undefined;
|
|
51
|
+
return typeof requestedName === "string" ? { kind, result, requestedName } : { kind, result };
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=executor-freeze.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Agent } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { PreparationPipeline } from "./executor-types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* The preparation steps an agent's loop runs today, read from the agent at call time: its resolver (lazy activation),
|
|
5
|
+
* its `beforeToolCall` / `afterToolCall` (the session's `tool_call` and `tool_result` chains) and its removed-tool
|
|
6
|
+
* hints. Building this pipeline installs nothing on the agent.
|
|
7
|
+
*/
|
|
8
|
+
export declare function agentPreparationPipeline(agent: Agent): PreparationPipeline;
|
|
9
|
+
//# sourceMappingURL=executor-pipeline.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { resolveToolNameAlias } from "@earendil-works/pi-agent-core";
|
|
2
|
+
/**
|
|
3
|
+
* The preparation steps an agent's loop runs today, read from the agent at call time: its resolver (lazy activation),
|
|
4
|
+
* its `beforeToolCall` / `afterToolCall` (the session's `tool_call` and `tool_result` chains) and its removed-tool
|
|
5
|
+
* hints. Building this pipeline installs nothing on the agent.
|
|
6
|
+
*/
|
|
7
|
+
export function agentPreparationPipeline(agent) {
|
|
8
|
+
return {
|
|
9
|
+
resolve: async (call, context) => {
|
|
10
|
+
const tools = context.tools ?? [];
|
|
11
|
+
const exact = tools.find((tool) => tool.name === call.name);
|
|
12
|
+
if (exact)
|
|
13
|
+
return exact;
|
|
14
|
+
const resolved = await agent.resolveUnknownToolCall?.(call.name, context);
|
|
15
|
+
if (resolved)
|
|
16
|
+
return resolved;
|
|
17
|
+
const aliased = resolveToolNameAlias(call.name, tools.map((tool) => tool.name));
|
|
18
|
+
return tools.find((tool) => tool.name === aliased);
|
|
19
|
+
},
|
|
20
|
+
beforeToolCall: async (context, signal) => agent.beforeToolCall?.(context, signal),
|
|
21
|
+
afterToolCall: async (context, signal) => agent.afterToolCall?.(context, signal),
|
|
22
|
+
removedToolHint: (name) => agent.removedToolHints?.[name],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=executor-pipeline.js.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { AfterToolCallContext, AfterToolCallResult, AgentContext, AgentTool, AgentToolCall, AgentToolResult, AgentToolUpdateCallback, BeforeToolCallContext, BeforeToolCallResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { ExecutionEntry, ExecutionKey, ExecutionLedger, FrozenCall } from "@earendil-works/pi-agent-core/conformance";
|
|
3
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
4
|
+
/** Today's per-call steps, as the executor runs them; the executor owns their order and the ledger around them. */
|
|
5
|
+
export interface PreparationPipeline {
|
|
6
|
+
/** Exact name, then the host resolver (which may activate a lazy tool), then the alias rule. */
|
|
7
|
+
resolve(call: AgentToolCall, context: AgentContext): Promise<AgentTool | undefined>;
|
|
8
|
+
/** The ordered `tool_call` chain; PreToolUse is one handler in it. */
|
|
9
|
+
beforeToolCall(context: BeforeToolCallContext, signal?: AbortSignal): Promise<BeforeToolCallResult | undefined>;
|
|
10
|
+
afterToolCall(context: AfterToolCallContext, signal?: AbortSignal): Promise<AfterToolCallResult | undefined>;
|
|
11
|
+
removedToolHint(name: string): string | undefined;
|
|
12
|
+
}
|
|
13
|
+
export interface PreparedExecutorOptions {
|
|
14
|
+
readonly ledger: ExecutionLedger;
|
|
15
|
+
/** The executor's authenticated identity: the actor of every executor-side ledger event. */
|
|
16
|
+
readonly executor: string;
|
|
17
|
+
readonly pipeline: PreparationPipeline;
|
|
18
|
+
readonly clock?: () => number;
|
|
19
|
+
}
|
|
20
|
+
export interface PrepareRequest {
|
|
21
|
+
readonly issuer: string;
|
|
22
|
+
readonly generation: number;
|
|
23
|
+
readonly workspace: string;
|
|
24
|
+
readonly scope: string;
|
|
25
|
+
/** The model's raw call; its id is the execution's command id. */
|
|
26
|
+
readonly call: AgentToolCall;
|
|
27
|
+
readonly assistantMessage: AssistantMessage;
|
|
28
|
+
readonly context: AgentContext;
|
|
29
|
+
/** Issuer-minted with the call, so a re-sent prepare matches its admission exactly. */
|
|
30
|
+
readonly admittedAt: number;
|
|
31
|
+
readonly leaseUntil: number;
|
|
32
|
+
readonly reconcilable?: boolean;
|
|
33
|
+
readonly signal?: AbortSignal;
|
|
34
|
+
}
|
|
35
|
+
/** `requestedName` is set when the model's name resolved to another tool; the caller adds today's notice. */
|
|
36
|
+
export type PrepareReply = {
|
|
37
|
+
readonly kind: "prepared";
|
|
38
|
+
readonly call: FrozenCall;
|
|
39
|
+
readonly requestedName?: string;
|
|
40
|
+
} | {
|
|
41
|
+
readonly kind: "blocked";
|
|
42
|
+
readonly result: AgentToolResult<unknown>;
|
|
43
|
+
readonly requestedName?: string;
|
|
44
|
+
} | {
|
|
45
|
+
readonly kind: "batch_abort";
|
|
46
|
+
readonly result: AgentToolResult<unknown>;
|
|
47
|
+
} | {
|
|
48
|
+
readonly kind: "refused";
|
|
49
|
+
readonly reason: string;
|
|
50
|
+
};
|
|
51
|
+
export interface ReleaseRequest {
|
|
52
|
+
readonly key: ExecutionKey;
|
|
53
|
+
readonly generation: number;
|
|
54
|
+
/** Verified identity of the releasing issuer, supplied by the endpoint. */
|
|
55
|
+
readonly actor: {
|
|
56
|
+
readonly id: string;
|
|
57
|
+
};
|
|
58
|
+
readonly signal?: AbortSignal;
|
|
59
|
+
readonly onUpdate?: AgentToolUpdateCallback<unknown>;
|
|
60
|
+
}
|
|
61
|
+
export type ReleaseReply = {
|
|
62
|
+
readonly kind: "settled";
|
|
63
|
+
readonly toolCall: AgentToolCall;
|
|
64
|
+
readonly result: AgentToolResult<unknown>;
|
|
65
|
+
readonly isError: boolean;
|
|
66
|
+
/** Set when the model's name resolved to another tool; the caller adds today's correction notice. */
|
|
67
|
+
readonly requestedName?: string;
|
|
68
|
+
} | {
|
|
69
|
+
readonly kind: "aborted";
|
|
70
|
+
readonly result: AgentToolResult<unknown>;
|
|
71
|
+
} | {
|
|
72
|
+
readonly kind: "replayed";
|
|
73
|
+
readonly entry: ExecutionEntry;
|
|
74
|
+
} | {
|
|
75
|
+
readonly kind: "refused";
|
|
76
|
+
readonly reason: string;
|
|
77
|
+
};
|
|
78
|
+
export interface PreparedExecutor {
|
|
79
|
+
prepare(request: PrepareRequest): Promise<PrepareReply>;
|
|
80
|
+
release(request: ReleaseRequest): Promise<ReleaseReply>;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=executor-types.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PreparedExecutor, PreparedExecutorOptions } from "./executor-types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The prepared executor (design 5.6.1): PREPARE persists effect_possible, runs today's resolution, shim, validation
|
|
4
|
+
* and `tool_call` chain once, and freezes the result; RELEASE dispatches the frozen call at most once and runs the
|
|
5
|
+
* after-hook once. Nothing here is installed on a session; a caller builds one and drives it.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createPreparedExecutor(options: PreparedExecutorOptions): PreparedExecutor;
|
|
8
|
+
//# sourceMappingURL=executor.d.ts.map
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { prepareAgentToolCall } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { errorResult, finalizeBody, messageOf, runBody } from "./executor-body.js";
|
|
3
|
+
import { digestOf, freezeCall, resultValue, stoppedReply, stoppedValue, toStoreValue, } from "./executor-freeze.js";
|
|
4
|
+
const keyId = ({ issuer, commandId }) => JSON.stringify([issuer, commandId]);
|
|
5
|
+
function refusal(reply) {
|
|
6
|
+
return { kind: "refused", reason: reply.status === "refused" ? reply.reason : reply.status };
|
|
7
|
+
}
|
|
8
|
+
// Today's message for a call the response cut off (agent-loop `createIncompleteToolCallErrorMessage`).
|
|
9
|
+
function incompleteMessage(name, errorMessage) {
|
|
10
|
+
if (errorMessage !== undefined) {
|
|
11
|
+
return `${errorMessage}${errorMessage.endsWith(".") ? "" : "."} Re-issue the tool call with complete arguments.`;
|
|
12
|
+
}
|
|
13
|
+
return `Tool call "${name}" was not executed: the response ended before the tool call was complete because it hit the output token limit. Re-issue the tool call with complete arguments.`;
|
|
14
|
+
}
|
|
15
|
+
/** A durable row that already passed admission: answer from it and never run preparation again. */
|
|
16
|
+
function fromRow(entry) {
|
|
17
|
+
const progress = entry.progress;
|
|
18
|
+
if ("call" in progress)
|
|
19
|
+
return { kind: "prepared", call: progress.call };
|
|
20
|
+
if (progress.state !== "settled")
|
|
21
|
+
return { kind: "refused", reason: progress.state };
|
|
22
|
+
const outcome = progress.outcome;
|
|
23
|
+
if (outcome.reason !== "blocked")
|
|
24
|
+
return { kind: "refused", reason: "terminal" };
|
|
25
|
+
return stoppedReply(outcome.value) ?? { kind: "refused", reason: "corrupt_blocked_outcome" };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The prepared executor (design 5.6.1): PREPARE persists effect_possible, runs today's resolution, shim, validation
|
|
29
|
+
* and `tool_call` chain once, and freezes the result; RELEASE dispatches the frozen call at most once and runs the
|
|
30
|
+
* after-hook once. Nothing here is installed on a session; a caller builds one and drives it.
|
|
31
|
+
*/
|
|
32
|
+
export function createPreparedExecutor(options) {
|
|
33
|
+
const { ledger, executor, pipeline } = options;
|
|
34
|
+
const clock = options.clock ?? Date.now;
|
|
35
|
+
const preparations = new Map();
|
|
36
|
+
const bodies = new Map();
|
|
37
|
+
const releases = new Map();
|
|
38
|
+
const step = (key, generation, event) => ledger.apply({ key, generation, actor: { id: executor }, now: clock(), event });
|
|
39
|
+
// A preparation that ends without a frozen call is terminal and durable, so a re-sent prepare (also from a new
|
|
40
|
+
// executor process after restart) gets this reply back instead of a restart-recovered `hand_restarted`.
|
|
41
|
+
async function stop(key, generation, reply) {
|
|
42
|
+
const recorded = await step(key, generation, { kind: "block", value: stoppedValue(reply) });
|
|
43
|
+
if (recorded.status === "applied")
|
|
44
|
+
return reply;
|
|
45
|
+
return recorded.status === "replayed" ? fromRow(recorded.entry) : refusal(recorded);
|
|
46
|
+
}
|
|
47
|
+
async function prepareOnce(request, requestDigest) {
|
|
48
|
+
const { call, generation, signal } = request;
|
|
49
|
+
const key = { issuer: request.issuer, commandId: call.id };
|
|
50
|
+
const admitted = await ledger.admit({
|
|
51
|
+
...key,
|
|
52
|
+
executor,
|
|
53
|
+
workspace: request.workspace,
|
|
54
|
+
scope: request.scope,
|
|
55
|
+
generation,
|
|
56
|
+
requestDigest,
|
|
57
|
+
admittedAt: request.admittedAt,
|
|
58
|
+
leaseUntil: request.leaseUntil,
|
|
59
|
+
reconcilable: request.reconcilable ?? false,
|
|
60
|
+
});
|
|
61
|
+
if (admitted.status === "replayed")
|
|
62
|
+
return fromRow(admitted.entry);
|
|
63
|
+
if (admitted.status !== "applied")
|
|
64
|
+
return refusal(admitted);
|
|
65
|
+
const preparing = await step(key, generation, { kind: "prepare" });
|
|
66
|
+
if (preparing.status !== "applied")
|
|
67
|
+
return refusal(preparing);
|
|
68
|
+
if (call.incomplete === true) {
|
|
69
|
+
return stop(key, generation, {
|
|
70
|
+
kind: "blocked",
|
|
71
|
+
result: errorResult(incompleteMessage(call.name, call.errorMessage)),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const effect = await step(key, generation, { kind: "effect" });
|
|
75
|
+
if (effect.status !== "applied")
|
|
76
|
+
return refusal(effect);
|
|
77
|
+
let tool;
|
|
78
|
+
try {
|
|
79
|
+
tool = await pipeline.resolve(call, request.context);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return stop(key, generation, { kind: "blocked", result: errorResult(messageOf(error)) });
|
|
83
|
+
}
|
|
84
|
+
if (!tool) {
|
|
85
|
+
const hint = pipeline.removedToolHint(call.name);
|
|
86
|
+
const text = hint === undefined ? `Tool ${call.name} not found` : `Tool ${call.name} not found. ${hint}`;
|
|
87
|
+
return stop(key, generation, { kind: "blocked", result: errorResult(text) });
|
|
88
|
+
}
|
|
89
|
+
const requestedName = tool.name === call.name ? undefined : call.name;
|
|
90
|
+
const named = requestedName === undefined ? {} : { requestedName };
|
|
91
|
+
try {
|
|
92
|
+
const prepared = prepareAgentToolCall(tool, requestedName === undefined ? call : { ...call, name: tool.name });
|
|
93
|
+
const before = await pipeline.beforeToolCall({
|
|
94
|
+
assistantMessage: request.assistantMessage,
|
|
95
|
+
toolCall: prepared.toolCall,
|
|
96
|
+
args: prepared.args,
|
|
97
|
+
context: request.context,
|
|
98
|
+
}, signal);
|
|
99
|
+
if (signal?.aborted)
|
|
100
|
+
return stop(key, generation, { kind: "batch_abort", result: errorResult("Operation aborted") });
|
|
101
|
+
if (before?.block) {
|
|
102
|
+
const result = errorResult(before.reason || "Tool execution was blocked");
|
|
103
|
+
if (before.terminate === true)
|
|
104
|
+
result.terminate = true;
|
|
105
|
+
return stop(key, generation, { kind: "blocked", result, ...named });
|
|
106
|
+
}
|
|
107
|
+
const frozen = freezeCall(tool.name, prepared.args);
|
|
108
|
+
const freeze = await step(key, generation, { kind: "freeze", call: frozen, awaitingConsent: false });
|
|
109
|
+
if (freeze.status !== "applied")
|
|
110
|
+
return refusal(freeze);
|
|
111
|
+
bodies.set(keyId(key), {
|
|
112
|
+
...named,
|
|
113
|
+
tool,
|
|
114
|
+
toolCall: prepared.toolCall,
|
|
115
|
+
args: frozen.args,
|
|
116
|
+
assistantMessage: request.assistantMessage,
|
|
117
|
+
context: request.context,
|
|
118
|
+
});
|
|
119
|
+
return { kind: "prepared", call: frozen, ...named };
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
return stop(key, generation, { kind: "blocked", result: errorResult(messageOf(error)), ...named });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function terminalRow(key) {
|
|
126
|
+
const entry = await ledger.read(key);
|
|
127
|
+
if (entry === undefined)
|
|
128
|
+
return { kind: "refused", reason: "not_found" };
|
|
129
|
+
const progress = entry.progress;
|
|
130
|
+
// A blocked call never had a body: release on it is refused, not replayed.
|
|
131
|
+
if (progress.state === "settled" && progress.outcome.reason === "blocked")
|
|
132
|
+
return { kind: "refused", reason: "blocked" };
|
|
133
|
+
return { kind: "replayed", entry };
|
|
134
|
+
}
|
|
135
|
+
async function runReleased(request, body) {
|
|
136
|
+
const { key, generation, signal } = request;
|
|
137
|
+
const dispatched = await step(key, generation, { kind: "dispatch" });
|
|
138
|
+
if (dispatched.status === "replayed")
|
|
139
|
+
return { kind: "replayed", entry: dispatched.entry };
|
|
140
|
+
if (dispatched.status !== "applied" || dispatched.entry.progress.state !== "dispatched") {
|
|
141
|
+
return refusal(dispatched);
|
|
142
|
+
}
|
|
143
|
+
const executed = await runBody(body, signal, request.onUpdate ?? (() => { }));
|
|
144
|
+
const { result, isError } = await finalizeBody(body, executed, pipeline, signal);
|
|
145
|
+
const value = resultValue(result, isError, body.requestedName);
|
|
146
|
+
const recorded = await step(key, generation, { kind: "result", value });
|
|
147
|
+
for (const memo of [bodies, preparations, releases])
|
|
148
|
+
memo.delete(keyId(key));
|
|
149
|
+
// A cancel or lease expiry that settled first stays terminal; the late body result is audit only.
|
|
150
|
+
if (recorded.status === "replayed")
|
|
151
|
+
return { kind: "replayed", entry: recorded.entry };
|
|
152
|
+
if (recorded.status !== "applied")
|
|
153
|
+
return refusal(recorded);
|
|
154
|
+
const named = body.requestedName === undefined ? {} : { requestedName: body.requestedName };
|
|
155
|
+
return { kind: "settled", toolCall: body.toolCall, result, isError, ...named };
|
|
156
|
+
}
|
|
157
|
+
// Every release reaches the ledger with its own actor and generation before any memo is read or written, so a
|
|
158
|
+
// wrong actor or an old generation is refused there and can never answer, or block, the issuer's own release.
|
|
159
|
+
async function release(request) {
|
|
160
|
+
const { key, generation } = request;
|
|
161
|
+
const released = await ledger.apply({
|
|
162
|
+
key,
|
|
163
|
+
generation,
|
|
164
|
+
actor: request.actor,
|
|
165
|
+
now: clock(),
|
|
166
|
+
event: { kind: "release" },
|
|
167
|
+
});
|
|
168
|
+
if (released.status === "refused")
|
|
169
|
+
return released.reason === "terminal" ? terminalRow(key) : refusal(released);
|
|
170
|
+
if (released.status === "result_expired")
|
|
171
|
+
return refusal(released);
|
|
172
|
+
const id = keyId(key);
|
|
173
|
+
const pending = releases.get(id);
|
|
174
|
+
if (pending !== undefined)
|
|
175
|
+
return pending;
|
|
176
|
+
const body = bodies.get(id);
|
|
177
|
+
if (body === undefined)
|
|
178
|
+
return { kind: "refused", reason: "not_prepared" };
|
|
179
|
+
// Today's loop never starts a body once the run is aborted; the call stays released for the issuer's abort.
|
|
180
|
+
if (request.signal?.aborted)
|
|
181
|
+
return { kind: "aborted", result: errorResult("Operation aborted") };
|
|
182
|
+
const reply = runReleased(request, body);
|
|
183
|
+
releases.set(id, reply);
|
|
184
|
+
void reply.catch(() => releases.get(id) === reply && releases.delete(id));
|
|
185
|
+
return reply;
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
prepare: (request) => {
|
|
189
|
+
const { name, arguments: args, incomplete, errorMessage } = request.call;
|
|
190
|
+
const requestDigest = digestOf(toStoreValue({ name, args, incomplete, errorMessage }));
|
|
191
|
+
const admission = JSON.stringify([
|
|
192
|
+
request.generation,
|
|
193
|
+
request.workspace,
|
|
194
|
+
request.scope,
|
|
195
|
+
requestDigest,
|
|
196
|
+
request.admittedAt,
|
|
197
|
+
request.leaseUntil,
|
|
198
|
+
request.reconcilable ?? false,
|
|
199
|
+
]);
|
|
200
|
+
const id = keyId({ issuer: request.issuer, commandId: request.call.id });
|
|
201
|
+
const pending = preparations.get(id);
|
|
202
|
+
if (pending !== undefined && pending.admission === admission)
|
|
203
|
+
return pending.reply;
|
|
204
|
+
if (pending !== undefined)
|
|
205
|
+
return Promise.resolve({ kind: "refused", reason: "identity_conflict" });
|
|
206
|
+
const reply = prepareOnce(request, requestDigest);
|
|
207
|
+
preparations.set(id, { admission, reply });
|
|
208
|
+
// Only a prepared call keeps its memo (release needs it); every other reply is durable or retryable.
|
|
209
|
+
const forget = () => preparations.get(id)?.reply === reply && preparations.delete(id);
|
|
210
|
+
void reply.then((settled) => settled.kind === "prepared" || forget(), forget);
|
|
211
|
+
return reply;
|
|
212
|
+
},
|
|
213
|
+
release,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=executor.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type AccountBlockRecord = {
|
|
2
|
+
readonly account: string;
|
|
3
|
+
readonly reason: string;
|
|
4
|
+
readonly until: number | undefined;
|
|
5
|
+
/** Whether the block was written to the credential store for every session, or held for this session only. */
|
|
6
|
+
readonly shared: boolean;
|
|
7
|
+
readonly detail: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Writes one `account_blocked` line to session.log for every block that is saved, so a pool that later
|
|
11
|
+
* refuses a request locally has a recorded cause (senpi#4671). The account is named by its seat digest,
|
|
12
|
+
* never its name (Desktop#4753); an unknown end time is left out rather than guessed.
|
|
13
|
+
*/
|
|
14
|
+
export declare function logAccountBlocked(record: AccountBlockRecord): void;
|
|
15
|
+
//# sourceMappingURL=account-block-log.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { seatDigest } from "./seat-digest.js";
|
|
2
|
+
import { logContinuityEvent } from "./session-observability.js";
|
|
3
|
+
const MAX_DETAIL_CHARS = 200;
|
|
4
|
+
/**
|
|
5
|
+
* Writes one `account_blocked` line to session.log for every block that is saved, so a pool that later
|
|
6
|
+
* refuses a request locally has a recorded cause (senpi#4671). The account is named by its seat digest,
|
|
7
|
+
* never its name (Desktop#4753); an unknown end time is left out rather than guessed.
|
|
8
|
+
*/
|
|
9
|
+
export function logAccountBlocked(record) {
|
|
10
|
+
logContinuityEvent("account_blocked", {
|
|
11
|
+
seat: seatDigest(record.account),
|
|
12
|
+
reason: record.reason,
|
|
13
|
+
...(record.until === undefined ? {} : { until: new Date(record.until).toISOString() }),
|
|
14
|
+
shared: record.shared,
|
|
15
|
+
error: record.detail.slice(0, MAX_DETAIL_CHARS),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=account-block-log.js.map
|
|
@@ -31,6 +31,12 @@ export type UnavailableAccount = {
|
|
|
31
31
|
};
|
|
32
32
|
/** The origin of an account pin, highest precedence first (`resolveClaudeAccountPin`). */
|
|
33
33
|
export type PinSource = "cli" | "settings" | "stored";
|
|
34
|
+
/** One pool member's state when the pool refused a request: why it is out, and until when. */
|
|
35
|
+
export type BlockedAccountState = {
|
|
36
|
+
readonly name: string;
|
|
37
|
+
readonly reason: string;
|
|
38
|
+
readonly until: number | undefined;
|
|
39
|
+
};
|
|
34
40
|
export declare class AllAccountsBlockedError extends Error {
|
|
35
41
|
readonly soonestUnblockAt: number | undefined;
|
|
36
42
|
/**
|
|
@@ -49,7 +55,9 @@ export declare class AllAccountsBlockedError extends Error {
|
|
|
49
55
|
readonly limitedModel: string | undefined;
|
|
50
56
|
/** Set only when account fallback is off, so the guidance names that account instead of the whole pool. */
|
|
51
57
|
readonly unavailableAccount: UnavailableAccount | undefined;
|
|
52
|
-
|
|
58
|
+
/** Every candidate's state, so the error names which account needs /login and which waits on a reset (senpi#4671). */
|
|
59
|
+
readonly accounts: readonly BlockedAccountState[];
|
|
60
|
+
constructor(soonestUnblockAt: number | undefined, blockReason?: "auth_error", limitedModel?: string, unavailableAccount?: UnavailableAccount, accounts?: readonly BlockedAccountState[]);
|
|
53
61
|
}
|
|
54
62
|
export declare function getAffinityKey(options: Pick<AffinityOptions, "affinityKey" | "sessionId">): string;
|
|
55
63
|
/**
|
|
@@ -2,14 +2,21 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { applyAccountOrder } from "@earendil-works/pi-ai/auth/pool/select";
|
|
3
3
|
import { activeModelBlockUntil, pruneModelBlocks } from "../../../credential-pool/model-scope.js";
|
|
4
4
|
export const DEFAULT_AFFINITY_KEY = "claude-sdk-oauth-default";
|
|
5
|
+
function describeAccountState(state) {
|
|
6
|
+
const until = state.until === undefined ? "re-login" : new Date(state.until).toISOString();
|
|
7
|
+
const login = state.reason === "auth_error" ? ` (run /login anthropic-subscription for ${state.name})` : "";
|
|
8
|
+
return `${state.name}: ${state.reason} until ${until}${login}`;
|
|
9
|
+
}
|
|
5
10
|
export class AllAccountsBlockedError extends Error {
|
|
6
|
-
constructor(soonestUnblockAt, blockReason, limitedModel, unavailableAccount) {
|
|
11
|
+
constructor(soonestUnblockAt, blockReason, limitedModel, unavailableAccount, accounts = []) {
|
|
7
12
|
const until = soonestUnblockAt === undefined ? undefined : new Date(soonestUnblockAt).toISOString();
|
|
8
|
-
|
|
13
|
+
const summary = limitedModel !== undefined && until !== undefined
|
|
9
14
|
? `All Anthropic Subscription accounts have hit the usage limit for model ${limitedModel} until ${until}.`
|
|
10
15
|
: until === undefined
|
|
11
16
|
? "All Anthropic Subscription accounts are blocked until re-login."
|
|
12
|
-
: `All Anthropic Subscription accounts are blocked until ${until}
|
|
17
|
+
: `All Anthropic Subscription accounts are blocked until ${until}.`;
|
|
18
|
+
super(accounts.length === 0 ? summary : `${summary} Accounts: ${accounts.map(describeAccountState).join("; ")}.`);
|
|
19
|
+
this.accounts = accounts;
|
|
13
20
|
this.limitedModel = limitedModel;
|
|
14
21
|
this.name = "AllAccountsBlockedError";
|
|
15
22
|
this.soonestUnblockAt = soonestUnblockAt;
|
|
@@ -119,6 +126,10 @@ export function selectAccount(accounts, options = {}) {
|
|
|
119
126
|
return afterClear;
|
|
120
127
|
throw new AllAccountsBlockedError(soonestUnblockAt(candidates, now, options.model), candidates.length > 0 && candidates.every((account) => account.blockReason === "auth_error")
|
|
121
128
|
? "auth_error"
|
|
122
|
-
: undefined, limitedModel(candidates, now, options.model), unavailableAccount)
|
|
129
|
+
: undefined, limitedModel(candidates, now, options.model), unavailableAccount, candidates.map((account) => ({
|
|
130
|
+
name: account.name,
|
|
131
|
+
reason: account.blockReason ?? (isAccountBlocked(account, now) ? "blocked" : "model_limit"),
|
|
132
|
+
until: unblockAt(account, now, options.model),
|
|
133
|
+
})));
|
|
123
134
|
}
|
|
124
135
|
//# sourceMappingURL=affinity.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CredentialStore } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { AccountSlot } from "./accounts.ts";
|
|
3
|
+
import { AllAccountsBlockedError } from "./affinity.ts";
|
|
3
4
|
import type { SdkErrorClassification } from "./errors.ts";
|
|
4
5
|
export declare const MAX_RATE_LIMIT_BLOCK_MS: number;
|
|
5
6
|
export declare const DEFAULT_RATE_LIMIT_BLOCK_MS = 60000;
|
|
@@ -49,7 +50,12 @@ export declare class ClassifiedSdkError extends Error {
|
|
|
49
50
|
readonly classification: SdkErrorClassification;
|
|
50
51
|
readonly original: unknown;
|
|
51
52
|
readonly suppressTurnRetry: boolean;
|
|
52
|
-
|
|
53
|
+
/**
|
|
54
|
+
* The pool's state when this failure left no other account to try (senpi#4671). The failure stays the
|
|
55
|
+
* error; the pool refusal is context, so a refresh failure or a 401 is never hidden behind "all blocked".
|
|
56
|
+
*/
|
|
57
|
+
readonly poolState: AllAccountsBlockedError | undefined;
|
|
58
|
+
constructor(classification: SdkErrorClassification, original: unknown, suppressTurnRetry: boolean, poolState?: AllAccountsBlockedError);
|
|
53
59
|
}
|
|
54
60
|
/**
|
|
55
61
|
* Runs one attempt per account for account-level failures (a limit, billing, auth), plus at most
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AUTH_BLOCK_REPROBE_MS } from "../../../credential-pool/auth-reprobe.js";
|
|
2
2
|
import { activeModelBlockUntil, mergeModelBlocks, modelBlockKey, pruneModelBlocks, withModelBlock, } from "../../../credential-pool/model-scope.js";
|
|
3
3
|
import { usageLimitResetMs } from "../../../credential-pool/reset-time.js";
|
|
4
|
+
import { logAccountBlocked } from "./account-block-log.js";
|
|
4
5
|
import { AllAccountsBlockedError, clearExpiredBlocks } from "./affinity.js";
|
|
5
6
|
import { isConfirmedAccountUsageLimit, usageLimitBlockMs } from "./usage-limit-block.js";
|
|
6
7
|
export const MAX_RATE_LIMIT_BLOCK_MS = 48 * 60 * 60 * 1_000;
|
|
@@ -91,13 +92,14 @@ export function forgetSessionBlocksFor(name) {
|
|
|
91
92
|
return lifted;
|
|
92
93
|
}
|
|
93
94
|
export class ClassifiedSdkError extends Error {
|
|
94
|
-
constructor(classification, original, suppressTurnRetry) {
|
|
95
|
+
constructor(classification, original, suppressTurnRetry, poolState) {
|
|
95
96
|
const detail = original instanceof Error ? original.message : String(original);
|
|
96
|
-
super(`${suppressTurnRetry ? TURN_RETRY_SUPPRESSION_PREFIX : ""}${detail}
|
|
97
|
+
super(`${suppressTurnRetry ? TURN_RETRY_SUPPRESSION_PREFIX : ""}${detail}`, poolState === undefined ? undefined : { cause: poolState });
|
|
97
98
|
this.name = "ClassifiedSdkError";
|
|
98
99
|
this.classification = classification;
|
|
99
100
|
this.original = original;
|
|
100
101
|
this.suppressTurnRetry = suppressTurnRetry;
|
|
102
|
+
this.poolState = poolState;
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
105
|
function record(value) {
|
|
@@ -356,6 +358,13 @@ export async function* runFailover(options) {
|
|
|
356
358
|
attempt--;
|
|
357
359
|
continue;
|
|
358
360
|
}
|
|
361
|
+
logAccountBlocked({
|
|
362
|
+
account: blocked.name,
|
|
363
|
+
reason: blocked.blockReason ?? "model_limit",
|
|
364
|
+
until: blocked.blockedUntil ?? activeModelBlockUntil(blocked.modelBlocks, options.model, now()),
|
|
365
|
+
shared,
|
|
366
|
+
detail: errorText(error),
|
|
367
|
+
});
|
|
359
368
|
accounts = replaceAccount(accounts, blocked);
|
|
360
369
|
const event = {
|
|
361
370
|
account: blocked,
|
|
@@ -363,14 +372,22 @@ export async function* runFailover(options) {
|
|
|
363
372
|
attempt: attempt + 1,
|
|
364
373
|
visibleDeltaEmitted,
|
|
365
374
|
};
|
|
375
|
+
let poolState;
|
|
366
376
|
try {
|
|
367
377
|
if (!visibleDeltaEmitted && attempt + 1 < attemptLimit) {
|
|
368
378
|
event.nextAccount = options.selectFn(accounts);
|
|
369
379
|
}
|
|
370
380
|
}
|
|
381
|
+
catch (selectError) {
|
|
382
|
+
if (!(selectError instanceof AllAccountsBlockedError))
|
|
383
|
+
throw selectError;
|
|
384
|
+
poolState = selectError;
|
|
385
|
+
}
|
|
371
386
|
finally {
|
|
372
387
|
await options.onFailover?.(event);
|
|
373
388
|
}
|
|
389
|
+
if (poolState !== undefined)
|
|
390
|
+
throw new ClassifiedSdkError(classification, error, visibleDeltaEmitted, poolState);
|
|
374
391
|
if (visibleDeltaEmitted)
|
|
375
392
|
throw classified;
|
|
376
393
|
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import type { PinSource, UnavailableAccount } from "./affinity.ts";
|
|
1
|
+
import type { BlockedAccountState, PinSource, UnavailableAccount } from "./affinity.ts";
|
|
2
2
|
import type { SdkErrorKind } from "./errors.ts";
|
|
3
3
|
import type { ClaudeCodeRun } from "./executable.ts";
|
|
4
4
|
export declare function noAccountGuidance(hasAnthropicCredential: boolean): string;
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The pool context of a failure that left no other account (senpi#4671): the failure stays the error, and this
|
|
7
|
+
* names the soonest retry (which goal mode reads) and every account's state. No refusal note: the pool did not
|
|
8
|
+
* refuse the request, the account failed it.
|
|
9
|
+
*/
|
|
10
|
+
export declare function exhaustedPoolGuidance(soonestUnblockAt: number | undefined, accounts: readonly BlockedAccountState[]): string;
|
|
11
|
+
export declare function allAccountsBlockedGuidance(soonestUnblockAt: number | undefined, blockReason?: "auth_error", accounts?: readonly BlockedAccountState[]): string;
|
|
6
12
|
/**
|
|
7
13
|
* How to move off a pinned account. `/claude-account pin` only writes the stored pin, which a CLI or a
|
|
8
14
|
* settings pin outranks (`resolveClaudeAccountPin`), so for those it would change nothing (senpi#3672).
|