@code-yeongyu/senpi 2026.11.10-2 → 2026.11.10-3
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 +22 -0
- package/dist/bundle/chunks/{args-77GYOPEO.js → args-HFYB6URH.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-YFZ734T3.js → azure-openai-responses-MNN2TINI.js} +1 -1
- package/dist/bundle/chunks/{chunk-ZUQCYIRY.js → chunk-2ODGHBUD.js} +1 -1
- package/dist/bundle/chunks/{chunk-MSDBBOB2.js → chunk-5H43O4MA.js} +1 -1
- package/dist/bundle/chunks/{chunk-IKJCA5SH.js → chunk-6Q7Y2G5A.js} +1 -1
- package/dist/bundle/chunks/{chunk-YCPURLPU.js → chunk-AC6MDRAE.js} +64 -64
- package/dist/bundle/chunks/{chunk-33MZJTQ3.js → chunk-B4WD5XHP.js} +1 -1
- package/dist/bundle/chunks/{chunk-V5FSENW6.js → chunk-COKX65TE.js} +1 -1
- package/dist/bundle/chunks/{chunk-YNGEZBTP.js → chunk-DEQLHN2J.js} +2 -2
- package/dist/bundle/chunks/{chunk-EJB5E2JK.js → chunk-DHAAWL2H.js} +1 -1
- package/dist/bundle/chunks/{chunk-2EJ4BGBM.js → chunk-DHORIIVQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-2M54EAAY.js → chunk-DIHIEJFV.js} +1 -1
- package/dist/bundle/chunks/{chunk-RCHIMH7N.js → chunk-DQY26AX3.js} +1 -1
- package/dist/bundle/chunks/{chunk-3Q5ZENSD.js → chunk-E6ZBTSC6.js} +1 -1
- package/dist/bundle/chunks/{chunk-74IB3JPP.js → chunk-H3VJVKUK.js} +4 -4
- package/dist/bundle/chunks/{chunk-OF3XVGG4.js → chunk-HZDKQOWB.js} +1 -1
- package/dist/bundle/chunks/chunk-IREUIMFE.js +11 -0
- package/dist/bundle/chunks/{chunk-TUEZNIBK.js → chunk-JSD6O3RJ.js} +2 -2
- package/dist/bundle/chunks/{chunk-UOZDDYBN.js → chunk-L3KVP7VJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-7MN7YNO5.js → chunk-L7DGTA3L.js} +1 -1
- package/dist/bundle/chunks/{chunk-J2NGJKXL.js → chunk-LXINMDMX.js} +1 -1
- package/dist/bundle/chunks/{chunk-QATU6BBC.js → chunk-NN7NESW2.js} +1 -1
- package/dist/bundle/chunks/{chunk-2U6HXWXS.js → chunk-NNQ34ZDW.js} +1 -1
- package/dist/bundle/chunks/chunk-NQKCJQHB.js +2 -0
- package/dist/bundle/chunks/{chunk-XH24X5B2.js → chunk-O5WJVIEU.js} +1 -1
- package/dist/bundle/chunks/{chunk-O3FG6HML.js → chunk-PJI5GT7U.js} +1 -1
- package/dist/bundle/chunks/{chunk-BPNSOZ2C.js → chunk-QUVSIW2R.js} +1 -1
- package/dist/bundle/chunks/{chunk-WXYMAUFX.js → chunk-QX7WD3IM.js} +1 -1
- package/dist/bundle/chunks/{chunk-Y4ONXQMR.js → chunk-RGHVPUUX.js} +1 -1
- package/dist/bundle/chunks/{chunk-RYPM5BZQ.js → chunk-S7NAJQB3.js} +1 -1
- package/dist/bundle/chunks/{chunk-LT353NSF.js → chunk-T4CUD7RF.js} +1 -1
- package/dist/bundle/chunks/{chunk-J7FW7MED.js → chunk-TCZORL3V.js} +1 -1
- package/dist/bundle/chunks/{chunk-KU72IKV5.js → chunk-VGSWOLIY.js} +1 -1
- package/dist/bundle/chunks/{chunk-Q4G5DECT.js → chunk-Z2C5N2LM.js} +2 -2
- package/dist/bundle/chunks/{cli-main-Y6ALA5OI.js → cli-main-ZTT3N4DO.js} +1 -1
- package/dist/bundle/chunks/{credential-accounts-LVVPKFEY.js → credential-accounts-5KHVGSCY.js} +1 -1
- package/dist/bundle/chunks/{dispatch-EP35KKJ3.js → dispatch-YUYZQXRP.js} +1 -1
- package/dist/bundle/chunks/{hand-entry-IPDAIOGI.js → hand-entry-UV5XPMBF.js} +3 -3
- package/dist/bundle/chunks/{headless-host-HKGNJGYT.js → headless-host-53UMJMM6.js} +1 -1
- package/dist/bundle/chunks/{health-write-FBF74QJ7.js → health-write-JLSF5EVV.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-5SCPY3OH.js → help-fast-path-552QLMWT.js} +1 -1
- package/dist/bundle/chunks/{host-command-2326GCUP.js → host-command-BZN7WYYV.js} +1 -1
- package/dist/bundle/chunks/{host-session-command-BZYBDOIE.js → host-session-command-O5WYHZXH.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-LBQB7DDR.js → interactive-mode-OCU4NDPC.js} +1 -1
- package/dist/bundle/chunks/{login-health-EVXVLHB2.js → login-health-TOSQT5RG.js} +1 -1
- package/dist/bundle/chunks/{main-FFRFP34W.js → main-CNDP6HCF.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-NDIHHJ2L.js → multi-session-host-ODGJAGJP.js} +1 -1
- package/dist/bundle/chunks/openai-codex-responses-VRGOAFEG.js +10 -0
- package/dist/bundle/chunks/{openai-responses-NIVBWE2H.js → openai-responses-I7PXRWCM.js} +2 -2
- package/dist/bundle/chunks/{package-manager-cli-XCPAMOYI.js → package-manager-cli-HSPUG4RJ.js} +1 -1
- package/dist/bundle/chunks/{role-HQUN4XXK.js → role-AEOLJRTP.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-F3BEMXBE.js → rotation-stream-E3FJFHXJ.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-TKNZM4PZ.js → rpc-mode-KOZPXU3U.js} +1 -1
- package/dist/bundle/chunks/{run-attached-tui-JDUKXDI2.js → run-attached-tui-4AENRP7P.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-HF2DHABB.js → schedule-command-SG44FE23.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-A6R6W2JQ.js → session-control-endpoint-5A24JYPE.js} +1 -1
- package/dist/bundle/chunks/{session-picker-GWA4S2PO.js → session-picker-73YRS3GE.js} +1 -1
- package/dist/bundle/chunks/{session-slot-CZGQQCOC.js → session-slot-RH7JLO3F.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +139 -139
- package/dist/bundle/chunks/{state-store-S7QAESIF.js → state-store-IECVTYXC.js} +1 -1
- package/dist/bundle/cli.js +2 -2
- 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.d.ts +12 -2
- package/dist/core/agent-session.js +59 -8
- package/dist/core/assistant-error-redaction.d.ts +9 -0
- package/dist/core/assistant-error-redaction.js +27 -0
- package/dist/core/config-reference-redaction.js +90 -10
- package/dist/core/context-files.d.ts +10 -0
- package/dist/core/context-files.js +58 -12
- package/dist/core/egress/wire.d.ts +13 -0
- package/dist/core/extensions/loader.js +24 -4
- package/dist/core/extensions/runner.d.ts +9 -1
- package/dist/core/extensions/runner.js +9 -4
- package/dist/core/extensions/types.d.ts +17 -3
- package/dist/core/hand/hook-advertise.d.ts +41 -0
- package/dist/core/hand/hook-advertise.js +98 -0
- package/dist/core/hand/hook-contract.d.ts +66 -0
- package/dist/core/hand/hook-contract.js +138 -0
- package/dist/core/resource-loader.js +2 -0
- package/dist/core/retry-fallback/log.d.ts +7 -1
- package/dist/core/retry-fallback/log.js +82 -5
- package/dist/core/retry-fallback/model-config-error.js +2 -1
- package/dist/core/settings-manager.d.ts +6 -0
- package/dist/core/settings-manager.js +8 -0
- package/dist/core/turn-origin.d.ts +19 -13
- package/dist/core/turn-origin.js +34 -30
- package/dist/core/ultrafast-lanes.d.ts +1 -0
- package/dist/core/ultrafast-lanes.js +4 -2
- package/dist/modes/interactive/components/footer.d.ts +23 -5
- package/dist/modes/interactive/components/footer.js +45 -16
- package/dist/modes/interactive/interactive-mode.js +9 -1
- package/dist/modes/rpc/admit-external-message-command.d.ts +2 -2
- package/dist/modes/rpc/admit-external-message-command.js +2 -10
- package/dist/modes/rpc/connection-handler.js +10 -6
- package/dist/modes/rpc/connection-roles.d.ts +2 -0
- package/dist/modes/rpc/connection-roles.js +4 -0
- package/dist/modes/rpc/custom-capability.d.ts +2 -0
- package/dist/modes/rpc/custom-capability.js +2 -0
- package/dist/modes/rpc/delegated-drive-origin.d.ts +6 -6
- package/dist/modes/rpc/delegated-drive-origin.js +7 -8
- package/dist/modes/rpc/delegation-declaration.d.ts +2 -0
- package/dist/modes/rpc/delegation-declaration.js +5 -1
- package/dist/modes/rpc/host-capabilities.js +3 -1
- package/dist/modes/rpc/loopless-tool-handlers.d.ts +6 -0
- package/dist/modes/rpc/loopless-tool-handlers.js +14 -1
- package/docs/extensions.md +10 -0
- package/docs/settings.md +1 -0
- package/docs/usage.md +1 -1
- package/native/socket-secret/prebuilds/win32-arm64/senpi_socket_secret.win32-arm64.node +0 -0
- package/native/socket-secret/prebuilds/win32-x64/senpi_socket_secret.win32-x64.node +0 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-DLAY7CIU.js +0 -2
- package/dist/bundle/chunks/chunk-XH2MWTXA.js +0 -11
- package/dist/bundle/chunks/openai-codex-responses-ABB2J43U.js +0 -10
|
@@ -1,21 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolved-value registry shared by every copy of this module in one JavaScript realm (omo#12965).
|
|
3
|
+
*
|
|
4
|
+
* A process can evaluate this module more than once (the bundle's shared chunk, its self-contained session-worker
|
|
5
|
+
* entry, and the SDK graph an in-process child loads), so the values are not kept in a module-local set: the first
|
|
6
|
+
* copy pins a frozen registry on `globalThis` under `SLOT_KEY` (non-writable, non-configurable, non-enumerable) and
|
|
7
|
+
* every later copy adopts it. The backing `Set` lives only in the installing copy's closure; the registry offers no
|
|
8
|
+
* delete, clear or setter, so in-realm code cannot remove a value, replace the slot or turn redaction off. Any
|
|
9
|
+
* in-realm code can `add` a string, which can only over-redact. This is a stopgap until one engine instance serves
|
|
10
|
+
* the whole process; other realms (workers, `vm` contexts, child processes) keep their own registry.
|
|
11
|
+
*
|
|
12
|
+
* Compatibility contract, permanent: the key string never changes. The v1 members `version` (1), `add(value)`,
|
|
13
|
+
* `size` (an accessor reporting the live count) and `values()` (a fresh array) are never removed or repurposed; a
|
|
14
|
+
* later contract keeps writing through this same slot and only adds members, and adopters ignore members they do not
|
|
15
|
+
* know. A copy adopts only a slot pinned the way `pin` pins it, frozen, with those four members; anything else (a
|
|
16
|
+
* slot planted before the first copy loaded, an accessor, another version) makes the import throw
|
|
17
|
+
* `ConfigReferenceRedactionSlotError`, because a copy running with a private set is the leak this registry removes.
|
|
18
|
+
*/
|
|
19
|
+
const SLOT_KEY_NAME = "@code-yeongyu/senpi/config-reference-redaction/v1";
|
|
20
|
+
const SLOT_KEY = Symbol.for(SLOT_KEY_NAME);
|
|
1
21
|
const MIN_REDACTED_LENGTH = 4;
|
|
2
|
-
|
|
3
|
-
|
|
22
|
+
/** The largest `$file` reference value (`config-reference-file.ts`); longer strings are never remembered. */
|
|
23
|
+
const MAX_REMEMBERED_LENGTH = 65_536;
|
|
24
|
+
class ConfigReferenceRedactionSlotError extends Error {
|
|
25
|
+
constructor(cause) {
|
|
26
|
+
super(`The process-wide redaction registry ${SLOT_KEY_NAME} was not installed by senpi or is incompatible; restart the process.`, cause === undefined ? undefined : { cause });
|
|
27
|
+
this.name = "ConfigReferenceRedactionSlotError";
|
|
28
|
+
this.code = "SENPI_REDACTION_SLOT_INCOMPATIBLE";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function createRegistry() {
|
|
32
|
+
const values = new Set();
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
version: 1,
|
|
35
|
+
add(value) {
|
|
36
|
+
if (typeof value !== "string" || value.length < MIN_REDACTED_LENGTH || value.length > MAX_REMEMBERED_LENGTH)
|
|
37
|
+
return;
|
|
38
|
+
values.add(value);
|
|
39
|
+
},
|
|
40
|
+
get size() {
|
|
41
|
+
return values.size;
|
|
42
|
+
},
|
|
43
|
+
values: () => [...values],
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function isFrozenV1Registry(value) {
|
|
47
|
+
return (typeof value === "object" &&
|
|
48
|
+
value !== null &&
|
|
49
|
+
Object.isFrozen(value) &&
|
|
50
|
+
Reflect.get(value, "version") === 1 &&
|
|
51
|
+
typeof Reflect.get(value, "add") === "function" &&
|
|
52
|
+
typeof Reflect.get(value, "values") === "function" &&
|
|
53
|
+
typeof Reflect.get(value, "size") === "number");
|
|
54
|
+
}
|
|
55
|
+
function pin(registry) {
|
|
56
|
+
try {
|
|
57
|
+
Object.defineProperty(globalThis, SLOT_KEY, {
|
|
58
|
+
value: registry,
|
|
59
|
+
writable: false,
|
|
60
|
+
configurable: false,
|
|
61
|
+
enumerable: false,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw new ConfigReferenceRedactionSlotError(error);
|
|
66
|
+
}
|
|
67
|
+
return registry;
|
|
68
|
+
}
|
|
69
|
+
function sharedRegistry() {
|
|
70
|
+
const slot = Reflect.getOwnPropertyDescriptor(globalThis, SLOT_KEY);
|
|
71
|
+
if (slot === undefined)
|
|
72
|
+
return pin(createRegistry());
|
|
73
|
+
const value = slot.value;
|
|
74
|
+
if (slot.writable !== false || slot.configurable !== false || !isFrozenV1Registry(value)) {
|
|
75
|
+
throw new ConfigReferenceRedactionSlotError();
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
const registry = sharedRegistry();
|
|
80
|
+
let redactionForms = [];
|
|
81
|
+
let formsSize = 0;
|
|
4
82
|
/** Every value a reference resolves to is kept for the process lifetime, including rotated ones: no cap, no eviction. */
|
|
5
83
|
export function rememberResolvedReferenceValue(value) {
|
|
6
|
-
|
|
7
|
-
return;
|
|
8
|
-
resolvedValues.add(value);
|
|
9
|
-
redactionForms = undefined;
|
|
84
|
+
registry.add(value);
|
|
10
85
|
}
|
|
11
86
|
function encodings(value) {
|
|
12
87
|
const form = new URLSearchParams([["", value]]).toString().slice(1);
|
|
13
88
|
return [value, JSON.stringify(value).slice(1, -1), encodeURIComponent(value), encodeURI(value), form];
|
|
14
89
|
}
|
|
90
|
+
/** The registry is append-only, so a changed size is the only way its content can change. */
|
|
15
91
|
function forms() {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
92
|
+
const size = registry.size;
|
|
93
|
+
if (size !== formsSize) {
|
|
94
|
+
redactionForms = [...new Set(registry.values().flatMap(encodings))]
|
|
95
|
+
.filter((form) => form.length >= MIN_REDACTED_LENGTH)
|
|
96
|
+
.sort((left, right) => right.length - left.length);
|
|
97
|
+
formsSize = size;
|
|
98
|
+
}
|
|
19
99
|
return redactionForms;
|
|
20
100
|
}
|
|
21
101
|
/** Replaces every resolved reference value, raw or encoded, before text reaches a sink outside the process. */
|
|
@@ -134,7 +214,7 @@ function isProtocolValue(key, value) {
|
|
|
134
214
|
return Object.hasOwn(PROTOCOL_VALUES, key) && PROTOCOL_VALUES[key]?.has(value) === true;
|
|
135
215
|
}
|
|
136
216
|
export function redactResolvedReferenceData(value) {
|
|
137
|
-
if (
|
|
217
|
+
if (registry.size === 0)
|
|
138
218
|
return value;
|
|
139
219
|
const copies = new WeakMap();
|
|
140
220
|
const pending = [];
|
|
@@ -16,6 +16,16 @@ export type ContextFiles = ContextFile[] & {
|
|
|
16
16
|
export interface ContextFileLimits {
|
|
17
17
|
maxBytesPerFile?: number;
|
|
18
18
|
maxTotalBytes?: number;
|
|
19
|
+
/**
|
|
20
|
+
* The global (user-scope) context file to load instead of the agent-dir probe (senpi#5567).
|
|
21
|
+
* Honored from the user's own settings only (`SettingsManager.getGlobalContextFilePath`); a
|
|
22
|
+
* project's settings never set it. A leading `~` is expanded; a relative path resolves against
|
|
23
|
+
* the agent dir, like other paths in global settings; a directory runs the same candidate probe
|
|
24
|
+
* as the agent dir. Unset or blank keeps the agent-dir file. A path that is set but missing,
|
|
25
|
+
* unreadable, a special file, or a directory with no candidate falls back to the agent-dir probe
|
|
26
|
+
* and reports one warning diagnostic instead of erroring.
|
|
27
|
+
*/
|
|
28
|
+
globalPath?: string;
|
|
19
29
|
}
|
|
20
30
|
export declare const DEFAULT_CONTEXT_FILE_MAX_BYTES: number;
|
|
21
31
|
export declare const DEFAULT_CONTEXT_FILES_MAX_TOTAL_BYTES: number;
|
|
@@ -29,6 +29,47 @@ function loadContextFileFromDir(dir) {
|
|
|
29
29
|
}
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* The global user-scope context file (senpi#5567). `contextFiles.globalPath` names a file (or a
|
|
34
|
+
* directory to probe for the usual candidates) to load in place of the agent-dir file. Precedence
|
|
35
|
+
* is defined and never errors: a blank or unset path keeps the agent-dir probe silently; a
|
|
36
|
+
* configured target that is missing, unreadable, a special file, or a directory with no candidates
|
|
37
|
+
* falls back to the agent-dir probe and returns one warning naming the configured path.
|
|
38
|
+
*/
|
|
39
|
+
function loadGlobalContextFile(agentDir, globalPath) {
|
|
40
|
+
const explicit = typeof globalPath === "string" ? globalPath.trim() : "";
|
|
41
|
+
if (explicit === "")
|
|
42
|
+
return { file: loadContextFileFromDir(agentDir) };
|
|
43
|
+
const resolved = resolvePath(explicit, agentDir);
|
|
44
|
+
const fallBack = (problem) => ({
|
|
45
|
+
file: loadContextFileFromDir(agentDir),
|
|
46
|
+
diagnostic: {
|
|
47
|
+
type: "warning",
|
|
48
|
+
path: resolved,
|
|
49
|
+
message: `contextFiles.globalPath ${problem}; using the global context file from ${agentDir} instead.`,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
let stats;
|
|
53
|
+
try {
|
|
54
|
+
stats = statSync(resolved);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
const code = error.code;
|
|
58
|
+
return fallBack(code === "ENOENT" || code === "ENOTDIR" ? "does not exist" : `could not be read (${String(error)})`);
|
|
59
|
+
}
|
|
60
|
+
if (stats.isDirectory()) {
|
|
61
|
+
const file = loadContextFileFromDir(resolved);
|
|
62
|
+
return file ? { file } : fallBack("is a directory with no AGENTS.md or CLAUDE.md");
|
|
63
|
+
}
|
|
64
|
+
if (!stats.isFile())
|
|
65
|
+
return fallBack("is not a regular file or directory");
|
|
66
|
+
try {
|
|
67
|
+
return { file: { path: resolved, content: stripBom(readFileSync(resolved, "utf-8")) } };
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
return fallBack(`could not be read (${String(error)})`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
32
73
|
/**
|
|
33
74
|
* The main repo's context file that a nested linked worktree's own copy shadows: both
|
|
34
75
|
* occupy the same logical repository scope, so loading both applies that context twice. Returns
|
|
@@ -142,21 +183,22 @@ function applyContextFileLimits(files, limits) {
|
|
|
142
183
|
}
|
|
143
184
|
return Object.assign(kept, { diagnostics });
|
|
144
185
|
}
|
|
145
|
-
/** Compares directories by their real path, ignoring case where the filesystem does (Windows). */
|
|
146
|
-
function
|
|
147
|
-
const real = canonicalizePath(resolvePath(
|
|
186
|
+
/** Compares files and directories by their real path, ignoring case where the filesystem does (Windows). */
|
|
187
|
+
function pathKey(path) {
|
|
188
|
+
const real = canonicalizePath(resolvePath(path));
|
|
148
189
|
return process.platform === "win32" ? real.toLowerCase() : real;
|
|
149
190
|
}
|
|
150
191
|
export function loadProjectContextFiles(options) {
|
|
151
192
|
const resolvedCwd = resolvePath(options.cwd);
|
|
152
193
|
const resolvedAgentDir = resolvePath(options.agentDir);
|
|
153
|
-
const homeKey =
|
|
194
|
+
const homeKey = pathKey(options.homeDir ?? homedir());
|
|
154
195
|
const contextFiles = [];
|
|
196
|
+
// Real-path keys: a symlinked ancestor file that points at an already loaded file is the same file.
|
|
155
197
|
const seenPaths = new Set();
|
|
156
|
-
const globalContext =
|
|
157
|
-
if (globalContext) {
|
|
158
|
-
contextFiles.push({ ...globalContext, scope: "user" });
|
|
159
|
-
seenPaths.add(globalContext.path);
|
|
198
|
+
const globalContext = loadGlobalContextFile(resolvedAgentDir, options.globalPath);
|
|
199
|
+
if (globalContext.file) {
|
|
200
|
+
contextFiles.push({ ...globalContext.file, scope: "user" });
|
|
201
|
+
seenPaths.add(pathKey(globalContext.file.path));
|
|
160
202
|
}
|
|
161
203
|
const ancestorContextFiles = [];
|
|
162
204
|
const shadowedContextFile = findShadowedContextFile(resolvedCwd);
|
|
@@ -164,9 +206,10 @@ export function loadProjectContextFiles(options) {
|
|
|
164
206
|
while (true) {
|
|
165
207
|
const contextFile = loadContextFileFromDir(currentDir);
|
|
166
208
|
const isShadowed = shadowedContextFile !== undefined && canonicalizePath(contextFile?.path ?? "") === shadowedContextFile;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
209
|
+
const contextFileKey = contextFile ? pathKey(contextFile.path) : undefined;
|
|
210
|
+
if (contextFile && contextFileKey !== undefined && !isShadowed && !seenPaths.has(contextFileKey)) {
|
|
211
|
+
ancestorContextFiles.unshift(pathKey(currentDir) === homeKey ? { ...contextFile, scope: "user" } : contextFile);
|
|
212
|
+
seenPaths.add(contextFileKey);
|
|
170
213
|
}
|
|
171
214
|
const parentDir = dirname(currentDir);
|
|
172
215
|
if (parentDir === currentDir)
|
|
@@ -174,6 +217,9 @@ export function loadProjectContextFiles(options) {
|
|
|
174
217
|
currentDir = parentDir;
|
|
175
218
|
}
|
|
176
219
|
contextFiles.push(...ancestorContextFiles);
|
|
177
|
-
|
|
220
|
+
const limited = applyContextFileLimits(contextFiles, options);
|
|
221
|
+
if (globalContext.diagnostic)
|
|
222
|
+
limited.diagnostics.unshift(globalContext.diagnostic);
|
|
223
|
+
return limited;
|
|
178
224
|
}
|
|
179
225
|
//# sourceMappingURL=context-files.js.map
|
|
@@ -168,11 +168,24 @@ export interface HookModelRequest {
|
|
|
168
168
|
readonly claim: SignedPlacementClaim;
|
|
169
169
|
readonly input: HookModelRequestInput;
|
|
170
170
|
}
|
|
171
|
+
/** A user hook's failure on the hand: reported with the reply, as a fused session reports it, never thrown. */
|
|
172
|
+
export interface HookFailure {
|
|
173
|
+
readonly extensionPath: string;
|
|
174
|
+
readonly event: string;
|
|
175
|
+
readonly error: string;
|
|
176
|
+
}
|
|
171
177
|
export interface HookModelRequestReply {
|
|
172
178
|
readonly t: "hook.model_request.reply";
|
|
173
179
|
readonly sessionId: string;
|
|
174
180
|
readonly stepId: string;
|
|
175
181
|
readonly output: HookModelRequestInput;
|
|
182
|
+
/** The hooks that failed; `output` is what the others produced. Absent: none failed. */
|
|
183
|
+
readonly errors?: readonly HookFailure[];
|
|
184
|
+
/** The messages and notices the hooks sent during the call, as on every loop-hook reply (S33). Absent: none. */
|
|
185
|
+
readonly sent?: readonly WireJson[];
|
|
186
|
+
readonly notices?: readonly WireJson[];
|
|
187
|
+
/** Late messages and notices the hand dropped past its per-session bound. Absent: none. */
|
|
188
|
+
readonly droppedLate?: number;
|
|
176
189
|
}
|
|
177
190
|
export interface HandSessionAdvertisement {
|
|
178
191
|
readonly t: "hand.session.advertise";
|
|
@@ -68,6 +68,19 @@ function assertExtensionSide(extension, runs, registration) {
|
|
|
68
68
|
return;
|
|
69
69
|
throw new Error(`Extension "${extension.path}" declared runs: ${JSON.stringify(runs)} for ${registration}; runs must be "brain" or "hand".`);
|
|
70
70
|
}
|
|
71
|
+
/** The events whose handlers may declare `{ providers }` (CK21). */
|
|
72
|
+
const PROVIDER_HOOK_EVENTS = [
|
|
73
|
+
"before_provider_request",
|
|
74
|
+
"provider_stream_event",
|
|
75
|
+
"after_provider_response",
|
|
76
|
+
];
|
|
77
|
+
function declaredProviders(extension, providers, registration) {
|
|
78
|
+
if (Array.isArray(providers) &&
|
|
79
|
+
providers.length > 0 &&
|
|
80
|
+
providers.every((provider) => typeof provider === "string" && provider !== ""))
|
|
81
|
+
return Object.freeze([...providers]);
|
|
82
|
+
throw new Error(`Extension "${extension.path}" declared providers: ${JSON.stringify(providers)} for ${registration}; providers must be a non-empty array of provider ids.`);
|
|
83
|
+
}
|
|
71
84
|
/** Modules available to extensions via virtualModules (for compiled binaries) */
|
|
72
85
|
const VIRTUAL_MODULES = {
|
|
73
86
|
typebox: _bundledTypebox,
|
|
@@ -355,11 +368,14 @@ function createExtensionAPI(extension, runtime, cwd, eventBus, session) {
|
|
|
355
368
|
on(event, handler, options) {
|
|
356
369
|
assertActive();
|
|
357
370
|
const runs = options?.runs;
|
|
358
|
-
if (runs !== undefined)
|
|
371
|
+
if (runs !== undefined)
|
|
359
372
|
assertExtensionSide(extension, runs, `"${event}"`);
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
373
|
+
// Checked on every side, also for a handler this side does not register: a malformed declaration always throws.
|
|
374
|
+
const providers = PROVIDER_HOOK_EVENTS.includes(event) && options?.providers !== undefined
|
|
375
|
+
? declaredProviders(extension, options.providers, `"${event}"`)
|
|
376
|
+
: undefined;
|
|
377
|
+
if (runs !== undefined && session.side !== undefined && runs !== session.side)
|
|
378
|
+
return () => { };
|
|
363
379
|
const registeredHandler = ownedBy(extension, (...args) => {
|
|
364
380
|
// A child event can run inside its parent's task-tool async scope.
|
|
365
381
|
// Bind the host context here, preserving registered-handler metadata.
|
|
@@ -376,6 +392,10 @@ function createExtensionAPI(extension, runtime, cwd, eventBus, session) {
|
|
|
376
392
|
extension.handlerSides ??= new WeakMap();
|
|
377
393
|
extension.handlerSides.set(registeredHandler, runs);
|
|
378
394
|
}
|
|
395
|
+
if (providers !== undefined) {
|
|
396
|
+
extension.handlerProviders ??= new WeakMap();
|
|
397
|
+
extension.handlerProviders.set(registeredHandler, providers);
|
|
398
|
+
}
|
|
379
399
|
if (event === "before_agent_start" && options?.previewSafe === true) {
|
|
380
400
|
extension.previewSafeHandlers ??= new WeakSet();
|
|
381
401
|
extension.previewSafeHandlers.add(registeredHandler);
|
|
@@ -129,6 +129,14 @@ export type ShutdownHandler = () => void;
|
|
|
129
129
|
export declare function emitSessionShutdownEvent(extensionRunner: ExtensionRunner, event: SessionShutdownEvent, options?: {
|
|
130
130
|
readonly processExit?: boolean;
|
|
131
131
|
}): Promise<boolean>;
|
|
132
|
+
/**
|
|
133
|
+
* A hand's dispatch of a hosted loop's request hooks (CK21): only the extensions `only` accepts, with the effective
|
|
134
|
+
* model the loop sent and no headers (they never leave the loop's side).
|
|
135
|
+
*/
|
|
136
|
+
export interface HandRequestHookDispatch {
|
|
137
|
+
readonly only: (extensionPath: string) => boolean;
|
|
138
|
+
readonly model?: Model<Api>;
|
|
139
|
+
}
|
|
132
140
|
export declare function emitProjectTrustEvent(extensionsResult: LoadExtensionsResult, event: ProjectTrustEvent, ctx: ProjectTrustContext): Promise<{
|
|
133
141
|
result?: ProjectTrustEventResult;
|
|
134
142
|
errors: ExtensionError[];
|
|
@@ -422,7 +430,7 @@ export declare class ExtensionRunner {
|
|
|
422
430
|
emitBeforeProviderRequest(payload: unknown, excludeExtensionPath?: string, request?: {
|
|
423
431
|
model: Model<Api>;
|
|
424
432
|
headers: ProviderHeaders;
|
|
425
|
-
}): Promise<unknown>;
|
|
433
|
+
}, hand?: HandRequestHookDispatch): Promise<unknown>;
|
|
426
434
|
emitBeforeProviderHeaders(headers: ProviderHeaders): Promise<ProviderHeaders>;
|
|
427
435
|
/** Use the loader's provenance, including bundled modules and generated global-default shims. */
|
|
428
436
|
getThirdPartySessionStartPaths(): string[];
|
|
@@ -2111,11 +2111,15 @@ export class ExtensionRunner {
|
|
|
2111
2111
|
transformHeaders: async (headers) => await this.emitBeforeProviderHeaders(headers),
|
|
2112
2112
|
};
|
|
2113
2113
|
}
|
|
2114
|
-
async emitBeforeProviderRequest(payload, excludeExtensionPath, request) {
|
|
2114
|
+
async emitBeforeProviderRequest(payload, excludeExtensionPath, request, hand) {
|
|
2115
2115
|
let currentPayload = payload;
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2116
|
+
// A hand running a hosted loop's request hooks (CK21) names whose handlers run; it has no loop of its own, so the
|
|
2117
|
+
// sessionless drop does not apply to that explicit dispatch. Every other caller keeps the dispatch it had.
|
|
2118
|
+
const requestHandlers = hand !== undefined
|
|
2119
|
+
? snapshotEventHandlers(this.extensions, "before_provider_request").filter(({ ext }) => hand.only(ext.path))
|
|
2120
|
+
: this.dropsLoopEvent("before_provider_request")
|
|
2121
|
+
? []
|
|
2122
|
+
: snapshotEventHandlers(this.extensions, "before_provider_request");
|
|
2119
2123
|
for (const { ext, handlers } of requestHandlers) {
|
|
2120
2124
|
if (ext.path === excludeExtensionPath)
|
|
2121
2125
|
continue;
|
|
@@ -2125,6 +2129,7 @@ export class ExtensionRunner {
|
|
|
2125
2129
|
type: "before_provider_request",
|
|
2126
2130
|
payload: currentPayload,
|
|
2127
2131
|
...(request ? { model: request.model, headers: request.headers } : {}),
|
|
2132
|
+
...(hand?.model === undefined ? {} : { model: hand.model }),
|
|
2128
2133
|
};
|
|
2129
2134
|
const handlerResult = await handler(event, this.createContext(ext.path));
|
|
2130
2135
|
if (handlerResult !== undefined) {
|
|
@@ -1473,6 +1473,18 @@ export interface HandlerRegistrationOptions {
|
|
|
1473
1473
|
*/
|
|
1474
1474
|
runs?: ExtensionSide;
|
|
1475
1475
|
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Registration options for the provider request and stream hooks (`before_provider_request`, `provider_stream_event`,
|
|
1478
|
+
* `after_provider_response`).
|
|
1479
|
+
*/
|
|
1480
|
+
export interface ProviderHookOptions extends HandlerRegistrationOptions {
|
|
1481
|
+
/**
|
|
1482
|
+
* The providers whose requests or streams this handler reads or changes; omitted, every provider. A hand advertises
|
|
1483
|
+
* these to a hosted brain so it knows which calls need the device's hooks (hosted model-key custody CK21). The
|
|
1484
|
+
* declaration never filters dispatch: the handler still receives every event wherever it runs.
|
|
1485
|
+
*/
|
|
1486
|
+
providers?: readonly ProviderId[];
|
|
1487
|
+
}
|
|
1476
1488
|
/** Registration options for `pi.on("before_agent_start", handler, options)`. */
|
|
1477
1489
|
export interface BeforeAgentStartHandlerOptions extends HandlerRegistrationOptions {
|
|
1478
1490
|
/**
|
|
@@ -2157,10 +2169,10 @@ export interface ExtensionAPI {
|
|
|
2157
2169
|
on(event: "session_tree", handler: ExtensionHandler<SessionTreeEvent>, options?: HandlerRegistrationOptions): () => void;
|
|
2158
2170
|
on(event: "context", handler: ExtensionHandler<ContextEvent, ContextEventResult>, options?: ContextHandlerOptions): () => void;
|
|
2159
2171
|
on(event: "context_with_system", handler: ExtensionHandler<ContextWithSystemEvent, ContextEventResult>, options?: ContextHandlerOptions): () => void;
|
|
2160
|
-
on(event: "before_provider_request", handler: ExtensionHandler<BeforeProviderRequestEvent, BeforeProviderRequestEventResult>, options?:
|
|
2172
|
+
on(event: "before_provider_request", handler: ExtensionHandler<BeforeProviderRequestEvent, BeforeProviderRequestEventResult>, options?: ProviderHookOptions): () => void;
|
|
2161
2173
|
on(event: "before_provider_headers", handler: ExtensionHandler<BeforeProviderHeadersEvent>, options?: HandlerRegistrationOptions): () => void;
|
|
2162
|
-
on(event: "after_provider_response", handler: ExtensionHandler<AfterProviderResponseEvent>, options?:
|
|
2163
|
-
on(event: "provider_stream_event", handler: ExtensionHandler<ProviderStreamEvent>, options?:
|
|
2174
|
+
on(event: "after_provider_response", handler: ExtensionHandler<AfterProviderResponseEvent>, options?: ProviderHookOptions): () => void;
|
|
2175
|
+
on(event: "provider_stream_event", handler: ExtensionHandler<ProviderStreamEvent>, options?: ProviderHookOptions): () => void;
|
|
2164
2176
|
on(event: "before_agent_start", handler: ExtensionHandler<BeforeAgentStartEvent, BeforeAgentStartEventResult>, options?: BeforeAgentStartHandlerOptions): () => void;
|
|
2165
2177
|
on(event: "agent_start", handler: ExtensionHandler<AgentStartEvent>, options?: HandlerRegistrationOptions): () => void;
|
|
2166
2178
|
on(event: "agent_end", handler: ExtensionHandler<AgentEndEvent>, options?: HandlerRegistrationOptions): () => void;
|
|
@@ -3057,6 +3069,8 @@ export interface Extension {
|
|
|
3057
3069
|
nonMutatingContextHandlers?: WeakSet<HandlerFn>;
|
|
3058
3070
|
/** The declared side of each handler registered with `{ runs }`; a handler that declared none has no entry. */
|
|
3059
3071
|
handlerSides?: WeakMap<HandlerFn, ExtensionSide>;
|
|
3072
|
+
/** The declared providers of each provider hook registered with `{ providers }`; undeclared hooks have no entry. */
|
|
3073
|
+
handlerProviders?: WeakMap<HandlerFn, readonly ProviderId[]>;
|
|
3060
3074
|
tools: Map<string, RegisteredTool>;
|
|
3061
3075
|
/** Optional for compatibility with extension records created before child modules. */
|
|
3062
3076
|
childModules?: Map<string, RegisteredChildModule>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ProviderId } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { HandSessionAdvertisement, HookModelRequest, HookModelRequestReply } from "../egress/wire.ts";
|
|
3
|
+
import type { Extension } from "../extensions/types.ts";
|
|
4
|
+
export type HookProviders = readonly ProviderId[] | "*";
|
|
5
|
+
export interface HandHookAccess {
|
|
6
|
+
readonly requestHooks: HookProviders;
|
|
7
|
+
readonly streamHooks: HookProviders;
|
|
8
|
+
}
|
|
9
|
+
/** The request and stream hooks of the extensions `only` accepts (the ones that run on this hand). */
|
|
10
|
+
export declare function handHookAccess(extensions: readonly Extension[], only: (extensionPath: string) => boolean): HandHookAccess;
|
|
11
|
+
export interface AdvertHashParts {
|
|
12
|
+
readonly extensionSetHash: string;
|
|
13
|
+
readonly cwd: string;
|
|
14
|
+
/** The project-local hook set, as hook identities; its order does not matter. */
|
|
15
|
+
readonly projectHooks: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
/** `H(extension set hash, cwd, project-local hook set)` (4.9): `sha256:` + hex SHA-256 of their canonical JSON. */
|
|
18
|
+
export declare function computeAdvertHash(parts: AdvertHashParts): string;
|
|
19
|
+
/** A bound session's advertisement, before a connection stamps it. */
|
|
20
|
+
export type HandHookSession = Omit<HandSessionAdvertisement, "t" | "connectionId">;
|
|
21
|
+
/** A hook request for a session not advertised on the connection it names; nothing ran. */
|
|
22
|
+
export declare class HookNotAdvertisedError extends Error {
|
|
23
|
+
readonly name = "HookNotAdvertisedError";
|
|
24
|
+
}
|
|
25
|
+
export interface HandHookLinkOptions {
|
|
26
|
+
send(message: HandSessionAdvertisement | HookModelRequestReply): Promise<void>;
|
|
27
|
+
serve(request: HookModelRequest): Promise<HookModelRequestReply>;
|
|
28
|
+
}
|
|
29
|
+
export interface HandHookLink {
|
|
30
|
+
/** A new connection: every bound session's advertisement goes out first, in order, under `connectionId`. */
|
|
31
|
+
connect(connectionId: string, sessions: readonly HandHookSession[]): Promise<void>;
|
|
32
|
+
/** A session bound (or re-hashed) mid-connection: its advertisement goes out before anything else for it. */
|
|
33
|
+
advertise(session: HandHookSession): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* One `hook.model_request` received on `connectionId`; its reply follows that session's advertisement. Its hooks run
|
|
36
|
+
* outside the outgoing order, so a slow hook holds back no other request's reply and no advertisement.
|
|
37
|
+
*/
|
|
38
|
+
receive(connectionId: string, request: HookModelRequest): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
export declare function createHandHookLink(options: HandHookLinkOptions): HandHookLink;
|
|
41
|
+
//# sourceMappingURL=hook-advertise.d.ts.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hand's hook advertisement of hosted model-key custody (row CK21; custody design 4.9, round-10 fold section 15).
|
|
3
|
+
*
|
|
4
|
+
* A hand tells a hosted brain, per bound session, which providers' calls need the device's hooks: the request hooks
|
|
5
|
+
* (`before_provider_request`) and the stream hooks (`provider_stream_event`, `after_provider_response`) of the user's
|
|
6
|
+
* extensions, each scoped by its `{ providers }` declaration or, undeclared, every provider (`"*"`). The advertisement
|
|
7
|
+
* carries the `connectionId`, the session's `cwd` and its `advertHash`, and it is the first message for its session on
|
|
8
|
+
* every (re)connect: the link serves no `hook.model_request` that its session's advertisement on that connection did
|
|
9
|
+
* not precede.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { canonicalJson } from "@earendil-works/pi-agent-core";
|
|
13
|
+
const REQUEST_HOOK_EVENTS = ["before_provider_request"];
|
|
14
|
+
const STREAM_HOOK_EVENTS = ["provider_stream_event", "after_provider_response"];
|
|
15
|
+
function hookProviders(extensions, events, only) {
|
|
16
|
+
const declared = new Set();
|
|
17
|
+
for (const extension of extensions) {
|
|
18
|
+
if (!only(extension.path))
|
|
19
|
+
continue;
|
|
20
|
+
for (const event of events)
|
|
21
|
+
for (const handler of extension.handlers.get(event) ?? []) {
|
|
22
|
+
const providers = extension.handlerProviders?.get(handler);
|
|
23
|
+
if (providers === undefined)
|
|
24
|
+
return "*";
|
|
25
|
+
for (const provider of providers)
|
|
26
|
+
declared.add(provider);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return [...declared].sort();
|
|
30
|
+
}
|
|
31
|
+
/** The request and stream hooks of the extensions `only` accepts (the ones that run on this hand). */
|
|
32
|
+
export function handHookAccess(extensions, only) {
|
|
33
|
+
return {
|
|
34
|
+
requestHooks: hookProviders(extensions, REQUEST_HOOK_EVENTS, only),
|
|
35
|
+
streamHooks: hookProviders(extensions, STREAM_HOOK_EVENTS, only),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** `H(extension set hash, cwd, project-local hook set)` (4.9): `sha256:` + hex SHA-256 of their canonical JSON. */
|
|
39
|
+
export function computeAdvertHash(parts) {
|
|
40
|
+
const canonical = canonicalJson({
|
|
41
|
+
cwd: parts.cwd,
|
|
42
|
+
extensionSetHash: parts.extensionSetHash,
|
|
43
|
+
projectHooks: [...parts.projectHooks].sort(),
|
|
44
|
+
});
|
|
45
|
+
return `sha256:${createHash("sha256").update(canonical, "utf8").digest("hex")}`;
|
|
46
|
+
}
|
|
47
|
+
/** A hook request for a session not advertised on the connection it names; nothing ran. */
|
|
48
|
+
export class HookNotAdvertisedError extends Error {
|
|
49
|
+
constructor() {
|
|
50
|
+
super(...arguments);
|
|
51
|
+
this.name = "HookNotAdvertisedError";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function createHandHookLink(options) {
|
|
55
|
+
let connectionId;
|
|
56
|
+
let advertised = new Set();
|
|
57
|
+
// One outgoing order: an advertisement queued before a reply is sent before it.
|
|
58
|
+
let outgoing = Promise.resolve();
|
|
59
|
+
const enqueue = (work) => {
|
|
60
|
+
const run = outgoing.then(work);
|
|
61
|
+
outgoing = run.then(() => undefined, () => undefined);
|
|
62
|
+
return run;
|
|
63
|
+
};
|
|
64
|
+
const sendAdvert = (connection, session, into) => enqueue(async () => {
|
|
65
|
+
if (connectionId !== connection)
|
|
66
|
+
return;
|
|
67
|
+
await options.send({ t: "hand.session.advertise", connectionId: connection, ...session });
|
|
68
|
+
into.add(session.sessionId);
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
async connect(next, sessions) {
|
|
72
|
+
connectionId = next;
|
|
73
|
+
const fresh = new Set();
|
|
74
|
+
advertised = fresh;
|
|
75
|
+
await Promise.all(sessions.map((session) => sendAdvert(next, session, fresh)));
|
|
76
|
+
},
|
|
77
|
+
async advertise(session) {
|
|
78
|
+
if (connectionId === undefined)
|
|
79
|
+
throw new HookNotAdvertisedError("no connection to advertise on");
|
|
80
|
+
await sendAdvert(connectionId, session, advertised);
|
|
81
|
+
},
|
|
82
|
+
async receive(connection, request) {
|
|
83
|
+
// Only the advertised check and the reply's send take a place in the outgoing order.
|
|
84
|
+
await enqueue(async () => {
|
|
85
|
+
if (connection !== connectionId || !advertised.has(request.sessionId))
|
|
86
|
+
throw new HookNotAdvertisedError(`hook.model_request for session ${JSON.stringify(request.sessionId)} was not preceded by its advertisement on connection ${JSON.stringify(connection)}`);
|
|
87
|
+
});
|
|
88
|
+
const reply = await options.serve(request);
|
|
89
|
+
await enqueue(async () => {
|
|
90
|
+
// A reconnect since the request arrived: its connection is gone, so its reply is not sent.
|
|
91
|
+
if (connection !== connectionId)
|
|
92
|
+
return;
|
|
93
|
+
await options.send(reply);
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=hook-advertise.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The request-hook contract of hosted model-key custody (row CK21; custody design 4.9, round-10 fold section 15).
|
|
3
|
+
*
|
|
4
|
+
* `HookRequestInput` is the serializable projection of the fused `before_provider_request` input: the provider payload
|
|
5
|
+
* and the effective request model, without the request headers or the model's own headers, which can carry a
|
|
6
|
+
* credential and never leave the side that dials. A step call has this input; an aux call has none. A hand serves
|
|
7
|
+
* `hook.model_request` by running its user's request hooks on the input, only after the request's placement claim
|
|
8
|
+
* verified, and replies with the payload they produced, with their failures and the messages and notices they sent.
|
|
9
|
+
*/
|
|
10
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
11
|
+
import type { ExecutionScope } from "@earendil-works/pi-agent-core";
|
|
12
|
+
import { type ClaimVerdict } from "../egress/placement-claim.ts";
|
|
13
|
+
import type { HookModelRequest, HookModelRequestReply, ProviderCallErrorCode, ProviderCallKind, WireJson } from "../egress/wire.ts";
|
|
14
|
+
import type { BeforeProviderRequestEvent } from "../extensions/types.ts";
|
|
15
|
+
/** The pinned shape of `HookModelRequestInput` (and of the reply's `output`). */
|
|
16
|
+
export type HookRequestInput = {
|
|
17
|
+
readonly payload: WireJson;
|
|
18
|
+
readonly model?: {
|
|
19
|
+
readonly [key: string]: WireJson;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
/** The input or output cannot cross the wire: it is not JSON, or not the closed envelope. */
|
|
23
|
+
export declare class HookContractError extends Error {
|
|
24
|
+
readonly name = "HookContractError";
|
|
25
|
+
}
|
|
26
|
+
/** The request's placement claim did not verify, so no hook ran; `code`/`retryable` are the claim's wire answer. */
|
|
27
|
+
export declare class HookRequestRefusedError extends Error {
|
|
28
|
+
readonly name = "HookRequestRefusedError";
|
|
29
|
+
readonly verdict: Exclude<ClaimVerdict, "valid">;
|
|
30
|
+
readonly code: ProviderCallErrorCode;
|
|
31
|
+
readonly retryable: boolean;
|
|
32
|
+
constructor(verdict: Exclude<ClaimVerdict, "valid">);
|
|
33
|
+
}
|
|
34
|
+
/** The projection of a fused `before_provider_request` input. */
|
|
35
|
+
export declare function hookRequestInputOf(event: Omit<BeforeProviderRequestEvent, "type"> & {
|
|
36
|
+
type?: string;
|
|
37
|
+
}): HookRequestInput;
|
|
38
|
+
/** A step call's hook input; an aux call runs no request hooks on the hand and has none. */
|
|
39
|
+
export declare function hookRequestInputForCall(call: ProviderCallKind, event: Omit<BeforeProviderRequestEvent, "type"> & {
|
|
40
|
+
type?: string;
|
|
41
|
+
}): HookRequestInput | undefined;
|
|
42
|
+
/** The reply's `output`: the payload the hooks produced, with the model the request carried. */
|
|
43
|
+
export declare function hookRequestOutputOf(input: HookRequestInput, payload: unknown): HookRequestInput;
|
|
44
|
+
/** Reads a wire value back as the closed envelope `{ payload, model? }`; the model never carries headers. */
|
|
45
|
+
export declare function parseHookRequestInput(value: unknown): HookRequestInput;
|
|
46
|
+
/** The effective model a hand hands its hooks, as the loop sent it. */
|
|
47
|
+
export declare function hookModelOf(input: HookRequestInput): Model<Api> | undefined;
|
|
48
|
+
/** What the user's request hooks produced: the reply's `output`, their failures and what they sent. */
|
|
49
|
+
export type HookRequestHooksResult = Omit<HookModelRequestReply, "t" | "sessionId" | "stepId" | "output"> & {
|
|
50
|
+
readonly output: HookRequestInput;
|
|
51
|
+
};
|
|
52
|
+
export interface HookServePorts {
|
|
53
|
+
/** The hand's placement-claim check (CK7's verifier with the hand's trust and clock). */
|
|
54
|
+
verifyClaim(claim: unknown): Promise<ClaimVerdict>;
|
|
55
|
+
/** Runs the user's request hooks of `sessionId` on `input` and returns what they produced. */
|
|
56
|
+
runRequestHooks(sessionId: string, input: HookRequestInput): Promise<HookRequestHooksResult>;
|
|
57
|
+
}
|
|
58
|
+
/** Serves one `hook.model_request`: claim first, then the closed input, then the hooks. */
|
|
59
|
+
export declare function serveHookModelRequest(request: HookModelRequest, ports: HookServePorts): Promise<HookModelRequestReply>;
|
|
60
|
+
/**
|
|
61
|
+
* `runRequestHooks` over a hand's loop-hook runner (`LooplessHand.loopHooks`): the session's hooks run on the runtime of
|
|
62
|
+
* `scope`, inside a loop-hook call bound to that session, with the loop busy (a model request is mid-turn). The hooks'
|
|
63
|
+
* failures and what they sent travel with the output, as they do on every loop-hook reply.
|
|
64
|
+
*/
|
|
65
|
+
export declare function loopHookRequestHooks(loopHooks: (event: string, payload: WireJson) => Promise<unknown>, scope: ExecutionScope): HookServePorts["runRequestHooks"];
|
|
66
|
+
//# sourceMappingURL=hook-contract.d.ts.map
|