@code-yeongyu/senpi 2026.11.10-1 → 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 +42 -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-YPAQXRP3.js → chunk-47T6YMI2.js} +2 -2
- package/dist/bundle/chunks/{chunk-I5ASHSLZ.js → chunk-4JMGWVRH.js} +1 -1
- package/dist/bundle/chunks/{chunk-MSDBBOB2.js → chunk-5H43O4MA.js} +1 -1
- package/dist/bundle/chunks/{chunk-3IYWH6RO.js → chunk-6Q7Y2G5A.js} +1 -1
- package/dist/bundle/chunks/{chunk-X4XWAB3J.js → chunk-AC6MDRAE.js} +81 -81
- 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-LHEP3ACI.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-GPXZTSEX.js → chunk-DIHIEJFV.js} +3 -3
- package/dist/bundle/chunks/{chunk-NH7SYIGN.js → chunk-DLPV4B3R.js} +1 -1
- package/dist/bundle/chunks/{chunk-RCHIMH7N.js → chunk-DQY26AX3.js} +1 -1
- package/dist/bundle/chunks/{chunk-Y7Z7XPUF.js → chunk-DZQA5LVN.js} +1 -1
- package/dist/bundle/chunks/{chunk-2CNSCMYC.js → chunk-E5O4SP5I.js} +1 -1
- package/dist/bundle/chunks/{chunk-3Q5ZENSD.js → chunk-E6ZBTSC6.js} +1 -1
- package/dist/bundle/chunks/{chunk-HI5JSLG5.js → chunk-H3VJVKUK.js} +4 -4
- package/dist/bundle/chunks/{chunk-L6NRDUWJ.js → chunk-HZDKQOWB.js} +1 -1
- package/dist/bundle/chunks/chunk-IREUIMFE.js +11 -0
- package/dist/bundle/chunks/{chunk-FAFIGBUD.js → chunk-JSD6O3RJ.js} +2 -2
- package/dist/bundle/chunks/{chunk-BELNOPAI.js → chunk-L3KVP7VJ.js} +3 -3
- 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-H33A33M4.js → chunk-M2VJVJMK.js} +1 -1
- package/dist/bundle/chunks/{chunk-ARUV3US4.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-TGW4R7SI.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-DBQBJTMF.js → chunk-VUY4BZDI.js} +1 -1
- package/dist/bundle/chunks/chunk-XI6THO4F.js +2 -0
- package/dist/bundle/chunks/{chunk-THYEZBUG.js → chunk-Z2C5N2LM.js} +2 -2
- package/dist/bundle/chunks/{cli-main-VG3C2EXI.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-3LQKUTYS.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-CCASS7W4.js → host-command-BZN7WYYV.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-WWC7HXQ6.js → host-lifecycle-TNHASSFJ.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +1 -1
- package/dist/bundle/chunks/{host-session-command-YJB2FLKU.js → host-session-command-O5WYHZXH.js} +1 -1
- package/dist/bundle/chunks/{host-socket-request-DMYXC56U.js → host-socket-request-QN4Q2P5E.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-CUN3WMUF.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-P7XRC2OI.js → main-CNDP6HCF.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-244X3P6L.js → multi-session-host-ODGJAGJP.js} +11 -8
- 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-UVWJOQA3.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-KOZPXU3U.js +2 -0
- package/dist/bundle/chunks/{rpc-monitor-source-C6VQ3BC4.js → rpc-monitor-source-PGTDSGWP.js} +1 -1
- package/dist/bundle/chunks/{run-attached-tui-R3RB5P7G.js → run-attached-tui-4AENRP7P.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-5FG2OKSR.js → schedule-command-SG44FE23.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-5A24JYPE.js +3 -0
- package/dist/bundle/chunks/{session-picker-2FZSNFS7.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 +199 -196
- package/dist/bundle/chunks/{state-store-S7QAESIF.js → state-store-IECVTYXC.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-runtime.d.ts +2 -0
- package/dist/core/agent-session-runtime.js +5 -0
- package/dist/core/agent-session.d.ts +13 -3
- package/dist/core/agent-session.js +89 -18
- 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/builtin/config-reload/change-groups.js +6 -2
- package/dist/core/extensions/builtin/mcp/auth/catalog-identity.js +32 -8
- package/dist/core/extensions/builtin/mcp/auth/oauth-grant.d.ts +43 -0
- package/dist/core/extensions/builtin/mcp/auth/oauth-grant.js +196 -0
- package/dist/core/extensions/builtin/mcp/auth/oauth-provider.d.ts +4 -17
- package/dist/core/extensions/builtin/mcp/auth/oauth-provider.js +64 -75
- package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.d.ts +1 -1
- package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.js +25 -4
- package/dist/core/extensions/builtin/mcp/auth/oauth.js +10 -7
- package/dist/core/extensions/builtin/mcp/auth/token-store.d.ts +18 -0
- package/dist/core/extensions/builtin/mcp/auth/token-store.js +3 -0
- package/dist/core/extensions/builtin/mcp/catalog-cache.js +7 -6
- package/dist/core/extensions/builtin/mcp/catalog.js +3 -2
- package/dist/core/extensions/builtin/mcp/invocation.js +6 -0
- package/dist/core/extensions/builtin/mcp/service-register.js +16 -2
- package/dist/core/extensions/builtin/mcp/service-tools-changed.js +61 -54
- package/dist/core/extensions/builtin/mcp/startup-race.d.ts +2 -0
- package/dist/core/extensions/builtin/mcp/startup-race.js +25 -7
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-2.js +6 -6
- 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/open-timing.d.ts +2 -0
- package/dist/core/open-timing.js +7 -1
- 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/session-manager.d.ts +2 -0
- package/dist/core/session-manager.js +15 -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/interactive/session-control-rpc-commands.d.ts +1 -1
- package/dist/modes/interactive/session-control-rpc-commands.js +5 -0
- 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/compact-admission-validation.js +4 -1
- package/dist/modes/rpc/connection-handler.d.ts +2 -0
- package/dist/modes/rpc/connection-handler.js +20 -8
- 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 +4 -0
- package/dist/modes/rpc/custom-capability.js +4 -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/effect-commands.d.ts +21 -0
- package/dist/modes/rpc/host-capabilities.d.ts +1 -0
- package/dist/modes/rpc/host-capabilities.js +5 -1
- package/dist/modes/rpc/host-observe-request.d.ts +2 -0
- package/dist/modes/rpc/host-observe-request.js +6 -1
- package/dist/modes/rpc/host-park-ledger.d.ts +19 -0
- package/dist/modes/rpc/host-park-ledger.js +110 -0
- package/dist/modes/rpc/lifecycle-delivery.js +4 -1
- package/dist/modes/rpc/loopless-tool-handlers.d.ts +6 -0
- package/dist/modes/rpc/loopless-tool-handlers.js +14 -1
- package/dist/modes/rpc/media-placeholders.js +1 -0
- package/dist/modes/rpc/method-classes-schema-host.d.ts +42 -1
- package/dist/modes/rpc/method-classes-schema-host.js +24 -21
- package/dist/modes/rpc/method-classes-schema.d.ts +42 -1
- package/dist/modes/rpc/method-classes-schema.js +5 -2
- package/dist/modes/rpc/method-classes.d.ts +2 -0
- package/dist/modes/rpc/method-classes.js +3 -0
- package/dist/modes/rpc/multi-session-host.js +1 -1
- package/dist/modes/rpc/rpc-host-command-types.d.ts +14 -1
- package/dist/modes/rpc/rpc-host-response-types.d.ts +21 -0
- package/dist/modes/rpc/session-attachment-gate.js +2 -0
- package/dist/modes/rpc/session-binding.d.ts +1 -0
- package/dist/modes/rpc/session-binding.js +6 -1
- package/dist/modes/rpc/session-connection-context.d.ts +2 -0
- package/dist/modes/rpc/session-connection-context.js +4 -0
- package/dist/modes/rpc/session-registry-attach.d.ts +2 -0
- package/dist/modes/rpc/session-registry-attach.js +15 -11
- package/dist/modes/rpc/session-registry-claims.js +2 -0
- package/dist/modes/rpc/session-registry-open.js +13 -7
- package/dist/modes/rpc/session-registry-opening.js +2 -1
- package/dist/modes/rpc/session-registry-types.d.ts +4 -0
- package/dist/modes/rpc/session-registry.d.ts +3 -0
- package/dist/modes/rpc/session-registry.js +27 -3
- package/dist/modes/rpc/session-router-close.js +1 -0
- package/dist/modes/rpc/session-router-context.d.ts +4 -2
- package/dist/modes/rpc/session-router-context.js +3 -0
- package/dist/modes/rpc/session-router-host-reads.js +1 -0
- package/dist/modes/rpc/session-router-occupancy.js +10 -1
- package/dist/modes/rpc/session-router-open-result.d.ts +12 -0
- package/dist/modes/rpc/session-router-open-result.js +63 -0
- package/dist/modes/rpc/session-router-open.d.ts +3 -0
- package/dist/modes/rpc/session-router-open.js +15 -63
- package/dist/modes/rpc/session-router-park.d.ts +15 -0
- package/dist/modes/rpc/session-router-park.js +176 -0
- package/dist/modes/rpc/session-router-routing.d.ts +1 -1
- package/dist/modes/rpc/session-router-routing.js +7 -0
- package/dist/modes/rpc/session-status-source.js +2 -2
- package/docs/engine-socket/fixtures/admission-routes/h-legacy.json +1 -1
- package/docs/engine-socket/fixtures/rpc-inbound-samples.json +22 -2
- package/docs/extensions.md +10 -0
- package/docs/rpc.md +50 -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-436DNN4Z.js +0 -2
- 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
- package/dist/bundle/chunks/rpc-mode-G4I3HHPB.js +0 -2
- package/dist/bundle/chunks/session-control-endpoint-XWCFLJYC.js +0 -3
|
@@ -3,7 +3,8 @@ import { isModernMcpClient } from "./modern/request.js";
|
|
|
3
3
|
import { hasSupportedModernToolSchemas } from "./modern/tool-schema.js";
|
|
4
4
|
import { collectAllPages } from "./expose/pagination.js";
|
|
5
5
|
export async function collectToolCatalog(server, connection, config, options = {}) {
|
|
6
|
-
const
|
|
6
|
+
const client = connection.client;
|
|
7
|
+
const result = await collectAllPages((cursor) => listMcpTools(client, cursor === undefined ? {} : { cursor }, { timeout: config.requestTimeoutMs }));
|
|
7
8
|
return result.items.map((tool) => ({
|
|
8
9
|
requestOrigin: options.requestOrigin,
|
|
9
10
|
annotations: tool.annotations,
|
|
@@ -17,7 +18,7 @@ export async function collectToolCatalog(server, connection, config, options = {
|
|
|
17
18
|
requestTimeoutMs: config.requestTimeoutMs,
|
|
18
19
|
schema: tool.inputSchema,
|
|
19
20
|
outputSchema: tool.outputSchema,
|
|
20
|
-
modern: isModernMcpClient(
|
|
21
|
+
modern: isModernMcpClient(client),
|
|
21
22
|
server,
|
|
22
23
|
tool: tool.name,
|
|
23
24
|
}));
|
|
@@ -61,6 +61,12 @@ export function createMcpInvocationResolver(scope) {
|
|
|
61
61
|
try {
|
|
62
62
|
await Promise.race([ready, interrupted]);
|
|
63
63
|
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
readySignal.throwIfAborted();
|
|
66
|
+
if (!scope.isCurrent())
|
|
67
|
+
return refuse("unavailable", "MCP session or server configuration was replaced.");
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
64
70
|
finally {
|
|
65
71
|
clearTimeout(timeout);
|
|
66
72
|
readySignal.removeEventListener("abort", abort);
|
|
@@ -3,7 +3,7 @@ import { computeMcpExposurePolicy } from "./expose/policy.js";
|
|
|
3
3
|
import { registerDirectMcpTools } from "./expose/session.js";
|
|
4
4
|
import { ensureMcpToolCallConnection } from "./health.js";
|
|
5
5
|
import { createMcpInvocationResolver } from "./invocation.js";
|
|
6
|
-
import { connectAndRefreshMcpCatalog } from "./startup-race.js";
|
|
6
|
+
import { connectAndRefreshMcpCatalog, prepareMcpAuthentication } from "./startup-race.js";
|
|
7
7
|
export async function registerMcpServiceDirectTools(pi, config, entries, toolSearchService, options = {}) {
|
|
8
8
|
return await registerDirectMcpTools(pi, config, [...entries].map((entry) => {
|
|
9
9
|
const serverConfig = config.servers[entry.name]?.config;
|
|
@@ -18,7 +18,21 @@ export async function registerMcpServiceDirectTools(pi, config, entries, toolSea
|
|
|
18
18
|
servesSession: options.servesSession,
|
|
19
19
|
isCurrent: () => entry.isCurrent?.() !== false && options.isCurrent?.(entry) !== false,
|
|
20
20
|
ready: async () => {
|
|
21
|
-
|
|
21
|
+
if (!(await prepareMcpAuthentication(entry)))
|
|
22
|
+
return;
|
|
23
|
+
if (entry.isCurrent?.() === false || options.isCurrent?.(entry) === false)
|
|
24
|
+
return;
|
|
25
|
+
if (entry.credentialsCurrent?.() === false) {
|
|
26
|
+
await entry.onCredentialsChanged?.();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
await ensureMcpToolCallConnection(entry.connection);
|
|
30
|
+
if (entry.isCurrent?.() === false || options.isCurrent?.(entry) === false)
|
|
31
|
+
return;
|
|
32
|
+
if (entry.credentialsCurrent?.() === false) {
|
|
33
|
+
await entry.onCredentialsChanged?.();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
22
36
|
await connectAndRefreshMcpCatalog(entry, serverConfig);
|
|
23
37
|
await entry.startupCatalogClaim?.settled();
|
|
24
38
|
if (entry.isCurrent?.() === false || options.isCurrent?.(entry) === false)
|
|
@@ -54,71 +54,78 @@ export async function refreshMcpToolsOnListChanged(entry, targets, config, conne
|
|
|
54
54
|
// The startup connect that still owns registration registers its refreshed catalog itself.
|
|
55
55
|
if (connectOnly && entry.startupCatalogClaim?.ownsRegistration() === true)
|
|
56
56
|
return;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return;
|
|
62
|
-
entry.cachedCatalog = refreshed;
|
|
63
|
-
}
|
|
64
|
-
const catalog = await collectToolCatalog(entry.name, entry.connection, server.config, {
|
|
65
|
-
agentDir: entry.agentDir,
|
|
66
|
-
outputGuard: config.settings.outputGuard,
|
|
67
|
-
});
|
|
68
|
-
if (!ownsCatalog())
|
|
69
|
-
return;
|
|
70
|
-
const newNames = mapMcpCatalogNames(catalog).map(({ name }) => name);
|
|
71
|
-
// Before the first refresh, the registered names are the catalog the startup pass registered.
|
|
72
|
-
const knownNames = entry.knownToolNames ??
|
|
73
|
-
(registeredCatalog === undefined
|
|
74
|
-
? newNames
|
|
75
|
-
: mapMcpCatalogNames(cachedToolsToCatalogEntries(entry.name, registeredCatalog, entry.connection, server.config.requestTimeoutMs, async () => { })).map(({ name }) => name));
|
|
76
|
-
const diff = diffMcpToolNames(knownNames, newNames);
|
|
77
|
-
const identity = mcpRegistrationIdentity(catalog, entry.cachedCatalog);
|
|
78
|
-
const initial = targets();
|
|
79
|
-
const stale = connectOnly ? initial.filter((target) => target.registeredIdentity() !== identity) : initial;
|
|
80
|
-
if (stale.length > 0) {
|
|
81
|
-
// Registration reads entry.cachedCatalog. A shared lease refreshed it above; nothing else
|
|
82
|
-
// refreshes a non-shared connection's catalog after its startup connect (#2188).
|
|
83
|
-
if (!(entry.connection instanceof SharedMcpLease)) {
|
|
84
|
-
const refreshed = await collectServerCatalogForCache(entry.connection, server.config, entry.generationHash ?? entry.configHash, entry.credentialIdentity);
|
|
57
|
+
try {
|
|
58
|
+
const registeredCatalog = entry.cachedCatalog;
|
|
59
|
+
if (entry.connection instanceof SharedMcpLease) {
|
|
60
|
+
const refreshed = await entry.connection.catalog();
|
|
85
61
|
if (!ownsCatalog())
|
|
86
62
|
return;
|
|
87
63
|
entry.cachedCatalog = refreshed;
|
|
88
|
-
await writeMcpCachedServer(entry.agentDir, entry.name, refreshed, ownsCatalog);
|
|
89
64
|
}
|
|
65
|
+
const catalog = await collectToolCatalog(entry.name, entry.connection, server.config, {
|
|
66
|
+
agentDir: entry.agentDir,
|
|
67
|
+
outputGuard: config.settings.outputGuard,
|
|
68
|
+
});
|
|
90
69
|
if (!ownsCatalog())
|
|
91
70
|
return;
|
|
92
|
-
|
|
93
|
-
//
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
71
|
+
const newNames = mapMcpCatalogNames(catalog).map(({ name }) => name);
|
|
72
|
+
// Before the first refresh, the registered names are the catalog the startup pass registered.
|
|
73
|
+
const knownNames = entry.knownToolNames ??
|
|
74
|
+
(registeredCatalog === undefined
|
|
75
|
+
? newNames
|
|
76
|
+
: mapMcpCatalogNames(cachedToolsToCatalogEntries(entry.name, registeredCatalog, entry.connection, server.config.requestTimeoutMs, async () => { })).map(({ name }) => name));
|
|
77
|
+
const diff = diffMcpToolNames(knownNames, newNames);
|
|
78
|
+
const identity = mcpRegistrationIdentity(catalog, entry.cachedCatalog);
|
|
79
|
+
const initial = targets();
|
|
80
|
+
const stale = connectOnly ? initial.filter((target) => target.registeredIdentity() !== identity) : initial;
|
|
81
|
+
if (stale.length > 0) {
|
|
82
|
+
// Registration reads entry.cachedCatalog. A shared lease refreshed it above; nothing else
|
|
83
|
+
// refreshes a non-shared connection's catalog after its startup connect (#2188).
|
|
84
|
+
if (!(entry.connection instanceof SharedMcpLease)) {
|
|
85
|
+
const refreshed = await collectServerCatalogForCache(entry.connection, server.config, entry.generationHash ?? entry.configHash, entry.credentialIdentity);
|
|
99
86
|
if (!ownsCatalog())
|
|
100
87
|
return;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
88
|
+
entry.cachedCatalog = refreshed;
|
|
89
|
+
await writeMcpCachedServer(entry.agentDir, entry.name, refreshed, ownsCatalog);
|
|
90
|
+
}
|
|
91
|
+
if (!ownsCatalog())
|
|
92
|
+
return;
|
|
93
|
+
// Tombstone removed tools BEFORE re-registration so the subsequent
|
|
94
|
+
// setActiveTools (which excludes them) leaves the tombstones inactive.
|
|
95
|
+
const seen = new Set(initial.map((target) => target.pi));
|
|
96
|
+
const failures = [];
|
|
97
|
+
let pending = stale;
|
|
98
|
+
while (pending.length > 0) {
|
|
99
|
+
for (const target of pending) {
|
|
100
|
+
if (!ownsCatalog())
|
|
101
|
+
return;
|
|
102
|
+
try {
|
|
103
|
+
for (const removed of diff.removed)
|
|
104
|
+
target.pi.registerTool(buildMcpTombstoneDefinition(removed, entry.name));
|
|
105
|
+
await target.register();
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
failures.push(error);
|
|
109
|
+
}
|
|
108
110
|
}
|
|
111
|
+
// A session that attached during this refresh registered the catalog cached before it.
|
|
112
|
+
pending = targets().filter((target) => !seen.has(target.pi));
|
|
113
|
+
for (const target of pending)
|
|
114
|
+
seen.add(target.pi);
|
|
115
|
+
}
|
|
116
|
+
if (failures.length > 0) {
|
|
117
|
+
entry.knownToolNames = newNames;
|
|
118
|
+
entry.lastListChangedDelta = formatMcpListChangedDelta(diff);
|
|
119
|
+
throw new AggregateError(failures, `MCP ${entry.name} tool refresh failed in ${failures.length} of ${seen.size} session(s)`);
|
|
109
120
|
}
|
|
110
|
-
// A session that attached during this refresh registered the catalog cached before it.
|
|
111
|
-
pending = targets().filter((target) => !seen.has(target.pi));
|
|
112
|
-
for (const target of pending)
|
|
113
|
-
seen.add(target.pi);
|
|
114
|
-
}
|
|
115
|
-
if (failures.length > 0) {
|
|
116
|
-
entry.knownToolNames = newNames;
|
|
117
|
-
entry.lastListChangedDelta = formatMcpListChangedDelta(diff);
|
|
118
|
-
throw new AggregateError(failures, `MCP ${entry.name} tool refresh failed in ${failures.length} of ${seen.size} session(s)`);
|
|
119
121
|
}
|
|
122
|
+
entry.knownToolNames = newNames;
|
|
123
|
+
entry.lastListChangedDelta = formatMcpListChangedDelta(diff);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (!ownsCatalog())
|
|
127
|
+
return;
|
|
128
|
+
throw error;
|
|
120
129
|
}
|
|
121
|
-
entry.knownToolNames = newNames;
|
|
122
|
-
entry.lastListChangedDelta = formatMcpListChangedDelta(diff);
|
|
123
130
|
}
|
|
124
131
|
//# sourceMappingURL=service-tools-changed.js.map
|
|
@@ -43,6 +43,8 @@ export declare class McpDeferredAttach {
|
|
|
43
43
|
}
|
|
44
44
|
export declare function raceMcpStartupConnect(options: RaceMcpStartupConnectOptions): Promise<void>;
|
|
45
45
|
export declare function connectAndRefreshMcpCatalog(entry: McpConnectionEntry, serverConfig: ResolvedMcpServer["config"]): Promise<void>;
|
|
46
|
+
/** Reconcile the grant before health recovery without attempting a transport connection. */
|
|
47
|
+
export declare function prepareMcpAuthentication(entry: McpConnectionEntry): Promise<boolean>;
|
|
46
48
|
export declare function ignoreStartupNeedsAuth(entry: McpConnectionEntry, connect: Promise<void>): Promise<void>;
|
|
47
49
|
export declare function waitForMcpStartupRace(connect: Promise<void>, deadlineMs?: number): Promise<McpStartupRaceResult>;
|
|
48
50
|
export declare function shouldRaceMcpStartup(lifecycle: "lazy" | "eager" | "keep-alive"): boolean;
|
|
@@ -91,15 +91,16 @@ export function connectAndRefreshMcpCatalog(entry, serverConfig) {
|
|
|
91
91
|
void refresh.then(finish, finish);
|
|
92
92
|
return refresh;
|
|
93
93
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
/** Reconcile the grant before health recovery without attempting a transport connection. */
|
|
95
|
+
export async function prepareMcpAuthentication(entry) {
|
|
96
|
+
if (entry.isCurrent?.() === false)
|
|
97
|
+
return false;
|
|
97
98
|
try {
|
|
98
99
|
await entry.authPlan?.refresh?.ensureFresh();
|
|
99
100
|
}
|
|
100
101
|
catch (error) {
|
|
101
|
-
if (entry.connection.state === "disabled")
|
|
102
|
-
return;
|
|
102
|
+
if (entry.isCurrent?.() === false || entry.connection.state === "disabled")
|
|
103
|
+
return false;
|
|
103
104
|
const authError = markMcpConnectionNeedsAuth(entry.connection, error);
|
|
104
105
|
if (authError !== undefined) {
|
|
105
106
|
entry.logger.warn(authError.message);
|
|
@@ -107,10 +108,17 @@ async function refreshConnectedMcpCatalog(entry, serverConfig) {
|
|
|
107
108
|
}
|
|
108
109
|
throw error;
|
|
109
110
|
}
|
|
111
|
+
if (entry.isCurrent?.() === false)
|
|
112
|
+
return false;
|
|
110
113
|
if (entry.credentialsCurrent?.() === false) {
|
|
111
114
|
await entry.onCredentialsChanged?.();
|
|
112
|
-
return;
|
|
115
|
+
return false;
|
|
113
116
|
}
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
async function refreshConnectedMcpCatalog(entry, serverConfig) {
|
|
120
|
+
if (serverConfig === undefined || !(await prepareMcpAuthentication(entry)))
|
|
121
|
+
return;
|
|
114
122
|
await connectMcpServer(entry.connection, entry.logger);
|
|
115
123
|
if (entry.connection.state !== "connected" || entry.isCurrent?.() === false)
|
|
116
124
|
return;
|
|
@@ -120,7 +128,15 @@ async function refreshConnectedMcpCatalog(entry, serverConfig) {
|
|
|
120
128
|
entry.connection.state === "connected" &&
|
|
121
129
|
entry.connection.generation === generation;
|
|
122
130
|
if (entry.connection instanceof SharedMcpLease) {
|
|
123
|
-
|
|
131
|
+
let catalog;
|
|
132
|
+
try {
|
|
133
|
+
catalog = await entry.connection.catalog();
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (!ownsCatalog())
|
|
137
|
+
return;
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
124
140
|
if (!ownsCatalog()) {
|
|
125
141
|
if (entry.credentialsCurrent?.() === false)
|
|
126
142
|
await entry.onCredentialsChanged?.();
|
|
@@ -150,6 +166,8 @@ async function refreshConnectedMcpCatalog(entry, serverConfig) {
|
|
|
150
166
|
await ensureMcpResourceSubscriptions(entry.connection.client, catalog.resources ?? []);
|
|
151
167
|
}
|
|
152
168
|
catch (error) {
|
|
169
|
+
if (!ownsCatalog())
|
|
170
|
+
return;
|
|
153
171
|
entry.logger.warn("Failed to refresh MCP catalog cache", {
|
|
154
172
|
error: error instanceof Error ? error.message : String(error),
|
|
155
173
|
});
|
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
// retries after a busy tool from nearly all tries to four in ten. "Blind
|
|
17
17
|
// retries" becomes the exact action it meant, repeating a failure that has not
|
|
18
18
|
// been diagnosed, so it no longer collides with "wait and retry".
|
|
19
|
-
// - Scope, structure ("Say who owns the code"):
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
19
|
+
// - Scope, structure ("Say who owns the code"): the guide's ownership sentence
|
|
20
|
+
// opens the structure paragraph and replaces the smallest-change sentence
|
|
21
|
+
// (owner order). Without it, 5.2 acts as a guest: parallel code, duplicates
|
|
22
|
+
// left in place, and nearby bugs only mentioned.
|
|
23
23
|
// - Working the Task, delegation ("Set expectations for subagents"): the 5.1
|
|
24
24
|
// sentence was a standing invitation to delegate, which the guide says to
|
|
25
25
|
// review first. It now states the cost before the invitation, what a brief
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// mannered-prose sentence. 5.2 reports run dense, so the rule asks for plain
|
|
30
30
|
// words, mostly short one-idea sentences, one word per thing, and no names
|
|
31
31
|
// made up during the work, for a reader switching in from something else.
|
|
32
|
-
// Deliberately absent:
|
|
32
|
+
// Deliberately absent: "answer directly" or think-less
|
|
33
33
|
// lines (the guide: no effect at medium effort and above, lower the effort
|
|
34
34
|
// instead); a token budget; the "treat that answer as done" lines; hard
|
|
35
35
|
// sentence-length limits; any instruction to write reasoning in the reply (a
|
|
@@ -77,7 +77,7 @@ The request sets the scope, and the scope is the deliverable: deliver all of it
|
|
|
77
77
|
|
|
78
78
|
Errors, timeouts, locked files, empty results, and failing tools are ordinary obstacles, not blockers: diagnose them, then work through them with the access you have (wait and retry a call that is safe to repeat, fix the request, use another tool or source) without asking first. A file marked not to be touched, withheld access, or a safety guardrail is a deliberate blocker: leave it alone and look for another safe way to finish. If part of the task stays blocked, finish every other part and say exactly what you left out and why.
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
This codebase is yours: what it's for and how it's built are your call, and you'll keep building on it. Add no helpers or abstractions for hypothetical needs and no defensive checks inside trusted code; validate only at system boundaries. A pre-existing bug or performance concern you notice is a follow-up for your summary, not a change in this diff. Scratch checks verify and get discarded; commit tests only where the task asks for them or the repository already keeps tests for that kind of change, sized like the neighboring test files. Prefer a surgical edit over rewriting a file when the result would be identical.
|
|
81
81
|
|
|
82
82
|
## Working the Task
|
|
83
83
|
|
|
@@ -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
|