Archived
feat(sessions): allow global-extension providers, require Pi 0.81
Relaxes the provider policy from 'global config only' to 'global sources': providers registered by agent-dir (global) extensions are learned once at daemon startup and allowed on the shared runtime; project-extension registrations are still rejected with a session warning. Global extensions load identically for every session, so their providers are daemon-consistent and cannot leak project state (#76). - Shim now allows allowlisted ids through and also covers Pi 0.81's native provider path (registerNativeProvider), closing a bypass. - Startup learning step loads only global extensions against a scratch cwd and diffs the runtime's registered provider ids. - Bumps @earendil-works/* dev/peer ranges to >=0.81.1 <0.82; adapts to the Agent.streamFn -> streamFunction rename. - Docs, changeset, unit and acceptance tests updated (global-extension allow path, late re-registration a la pi-tensorx, native provider rule).
This commit is contained in:
@@ -7,7 +7,7 @@ import { WorkspaceActivityService } from "./activity/workspaceActivityService.js
|
||||
import { registerWorkspaceActivityRoutes } from "./activity/workspaceActivityRoutes.js";
|
||||
import { SessionEventHub } from "./realtime/sessionEventHub.js";
|
||||
import { AuthService } from "./sessions/authService.js";
|
||||
import { installGlobalProviderPolicy } from "./sessions/globalProviderPolicy.js";
|
||||
import { installGlobalProviderPolicy, learnGlobalExtensionProviderIds } from "./sessions/globalProviderPolicy.js";
|
||||
import { registerAuthRoutes } from "./sessions/authRoutes.js";
|
||||
import { PiSessionService } from "./sessions/piSessionService.js";
|
||||
import { createPiSessionManagerGateway } from "./sessions/piSessionManagerGateway.js";
|
||||
@@ -70,9 +70,12 @@ await runSessionDaemonStartup({
|
||||
}),
|
||||
});
|
||||
auth.subscribe((change) => { sessions.applyAuthChange(change); });
|
||||
// PI WEB only supports globally configured providers: reject every
|
||||
// PI WEB providers come from global sources only. Learn which providers
|
||||
// the agent directory's global extensions register (they are identical
|
||||
// for every session, hence daemon-safe), then reject every other
|
||||
// extension provider registration against the shared daemon-wide runtime.
|
||||
installGlobalProviderPolicy(auth.runtime, (providerId) => { sessions.noteRejectedProviderRegistration(providerId); });
|
||||
const globalExtensionProviderIds = await learnGlobalExtensionProviderIds(auth.runtime, activeAgentProfile.dir);
|
||||
installGlobalProviderPolicy(auth.runtime, globalExtensionProviderIds, (providerId) => { sessions.noteRejectedProviderRegistration(providerId); });
|
||||
const terminals = new TerminalService(eventHub, workspaceActivity);
|
||||
const runtimeComponent = Object.freeze({
|
||||
...getPiWebRuntimeComponent("sessiond", SESSIOND_RUNTIME_CAPABILITIES),
|
||||
|
||||
@@ -4,22 +4,24 @@ import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
||||
import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
|
||||
import { installGlobalProviderPolicy, providerRejectionMessage } from "./globalProviderPolicy.js";
|
||||
import { installGlobalProviderPolicy, learnGlobalExtensionProviderIds, providerRejectionMessage } from "./globalProviderPolicy.js";
|
||||
import { createPiSessionManagerGateway } from "./piSessionManagerGateway.js";
|
||||
import { PiSessionService } from "./piSessionService.js";
|
||||
import { CapturingSessionEventHub, createTestModelRuntime, TEST_MODEL_ID, TEST_MODEL_PROVIDER } from "./piSessionService.testSupport.js";
|
||||
|
||||
/**
|
||||
* Acceptance tests for the global provider policy, wired exactly as sessiond
|
||||
* wires it in production: one shared ModelRuntime per daemon, the policy shim
|
||||
* installed on it, and rejections fed into the session service. Sessions are
|
||||
* created through the real default runtime factory, so project extensions in a
|
||||
* temp cwd are genuinely loaded by Pi's `createAgentSessionServices`.
|
||||
* wires it in production: one shared ModelRuntime per daemon, the global
|
||||
* extensions' provider ids learned at startup, the policy shim installed on
|
||||
* the runtime, and rejections fed into the session service. Sessions are
|
||||
* created through the real default runtime factory, so extensions in a temp
|
||||
* cwd or temp agent dir are genuinely loaded by Pi's
|
||||
* `createAgentSessionServices`.
|
||||
*
|
||||
* These tests are also the tripwire for the shim's one piece of machinery
|
||||
* (instance-method shadowing of `registerProvider`): if a Pi upgrade changes
|
||||
* how registrations reach the runtime, the load-time and late-registration
|
||||
* tests here fail loudly.
|
||||
* (instance-method shadowing of `registerProvider` / `registerNativeProvider`
|
||||
* / `unregisterProvider`): if a Pi upgrade changes how registrations reach
|
||||
* the runtime, the load-time and late-registration tests here fail loudly.
|
||||
*/
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
@@ -56,11 +58,21 @@ async function policyHarness(options: { runtime?: ModelRuntime; agentDir?: strin
|
||||
heartbeatIntervalMs: 60_000,
|
||||
});
|
||||
services.push(service);
|
||||
// The exact sessiond wiring: policy on the shared runtime, rejections to the service.
|
||||
installGlobalProviderPolicy(runtime, (providerId) => { service.noteRejectedProviderRegistration(providerId); });
|
||||
// The exact sessiond wiring: learn the agent dir's global-extension
|
||||
// providers first, then the policy on the shared runtime, rejections to the
|
||||
// service.
|
||||
const allowedProviderIds = await learnGlobalExtensionProviderIds(runtime, agentDir);
|
||||
installGlobalProviderPolicy(runtime, allowedProviderIds, (providerId) => { service.noteRejectedProviderRegistration(providerId); });
|
||||
return { service, runtime, agentDir };
|
||||
}
|
||||
|
||||
/** Write a global extension into `<agentDir>/extensions/` (agent-dir extensions load for every session). */
|
||||
async function agentDirWithExtension(agentDir: string, source: string): Promise<string> {
|
||||
await mkdir(join(agentDir, "extensions"), { recursive: true });
|
||||
await writeFile(join(agentDir, "extensions", "global-probe.js"), source);
|
||||
return agentDir;
|
||||
}
|
||||
|
||||
/** Write a project extension into `<cwd>/.pi/extensions/` and return the cwd. */
|
||||
async function projectWithExtension(source: string): Promise<string> {
|
||||
const cwd = await tempDir("pi-web-policy-project-");
|
||||
@@ -97,7 +109,7 @@ function providerRegistrationSource(providerId: string): string {
|
||||
return `pi.registerProvider(${JSON.stringify(providerId)}, ${providerConfigJson(providerId)});`;
|
||||
}
|
||||
|
||||
const POLICY_WORDING = "PI WEB only supports globally configured providers";
|
||||
const POLICY_WORDING = "PI WEB providers must come from global configuration";
|
||||
|
||||
describe("global provider policy acceptance", () => {
|
||||
it("rejects a load-time provider registration while the extension's tool and command keep working", async () => {
|
||||
@@ -231,6 +243,59 @@ describe("global provider policy acceptance", () => {
|
||||
expect(runtime.getModel("collide-acme", "model-1")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("allows providers from global (agent-dir) extensions while still rejecting project extensions", async () => {
|
||||
const agentDir = await agentDirWithExtension(await tempDir("pi-web-policy-agent-"), `
|
||||
export default function (pi) {
|
||||
${providerRegistrationSource("global-ext")}
|
||||
}
|
||||
`);
|
||||
const { service, runtime } = await policyHarness({ agentDir });
|
||||
const cwd = await projectWithExtension(`
|
||||
export default function (pi) {
|
||||
${providerRegistrationSource("project-ext")}
|
||||
}
|
||||
`);
|
||||
|
||||
const session = await service.start(cwd);
|
||||
const ref = { id: session.id, cwd };
|
||||
|
||||
// The global extension's provider reached the shared runtime at daemon
|
||||
// startup and stays usable; the project extension's is rejected.
|
||||
expect(runtime.getModel("global-ext", "model-1")).toBeDefined();
|
||||
expect(runtime.getModel("project-ext", "model-1")).toBeUndefined();
|
||||
expect(runtime.getRegisteredProviderIds()).toEqual(["global-ext"]);
|
||||
const status = await service.status(ref);
|
||||
expect(status.warnings).toEqual([
|
||||
{ severity: "warning", message: providerRejectionMessage("project-ext", cwd), source: "runtime" },
|
||||
]);
|
||||
const models = await service.availableModels(ref);
|
||||
expect(models.some((model) => model.provider === "global-ext")).toBe(true);
|
||||
expect(models.some((model) => model.provider === "project-ext")).toBe(false);
|
||||
});
|
||||
|
||||
it("lets a global extension re-register its provider late without warnings", async () => {
|
||||
// The pi-tensorx pattern: register on load, then re-register from
|
||||
// session_start with a refreshed model catalog. Both calls carry the
|
||||
// learned id, so neither is a leak.
|
||||
const agentDir = await agentDirWithExtension(await tempDir("pi-web-policy-agent-"), `
|
||||
export default function (pi) {
|
||||
${providerRegistrationSource("global-ext")}
|
||||
pi.on("session_start", () => {
|
||||
${providerRegistrationSource("global-ext")}
|
||||
});
|
||||
}
|
||||
`);
|
||||
const { service, runtime } = await policyHarness({ agentDir });
|
||||
const plainCwd = await tempDir("pi-web-policy-project-");
|
||||
|
||||
const bystander = await service.start(plainCwd);
|
||||
const status = await service.status({ id: bystander.id, cwd: plainCwd });
|
||||
expect((status.warnings ?? []).filter((warning) => warning.message.includes(POLICY_WORDING))).toEqual([]);
|
||||
const inbox = service.notificationInbox({ id: bystander.id, cwd: plainCwd });
|
||||
expect(inbox.notifications.filter((notification) => notification.message.includes(POLICY_WORDING))).toEqual([]);
|
||||
expect(runtime.getModel("global-ext", "model-1")).toBeDefined();
|
||||
});
|
||||
|
||||
it("does not let a project-level models.json alter the shared runtime's provider set", async () => {
|
||||
// Spike assertion for plan §2: the shared runtime reads providers from the
|
||||
// agent-dir models.json only. A project-level models.json is not a
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
||||
import type { Provider } from "@earendil-works/pi-ai";
|
||||
import { installGlobalProviderPolicy, providerRejectionMessage } from "./globalProviderPolicy.js";
|
||||
import { createTestModelRuntime, TEST_MODEL_ID, TEST_MODEL_PROVIDER } from "./piSessionService.testSupport.js";
|
||||
|
||||
@@ -8,15 +9,33 @@ import { createTestModelRuntime, TEST_MODEL_ID, TEST_MODEL_PROVIDER } from "./pi
|
||||
* every test builds a dedicated runtime rather than touching the shared
|
||||
* `testModelRuntime` from testSupport.
|
||||
*/
|
||||
async function policyRuntime(): Promise<{ runtime: ModelRuntime; rejections: string[] }> {
|
||||
async function policyRuntime(
|
||||
allowedExtensionProviderIds: ReadonlySet<string> = new Set(),
|
||||
): Promise<{ runtime: ModelRuntime; rejections: string[] }> {
|
||||
const runtime = await createTestModelRuntime();
|
||||
const rejections: string[] = [];
|
||||
installGlobalProviderPolicy(runtime, (providerId) => { rejections.push(providerId); });
|
||||
installGlobalProviderPolicy(runtime, allowedExtensionProviderIds, (providerId) => { rejections.push(providerId); });
|
||||
return { runtime, rejections };
|
||||
}
|
||||
|
||||
function nativeProvider(providerId: string): Provider {
|
||||
return {
|
||||
id: providerId,
|
||||
name: providerId,
|
||||
auth: {
|
||||
apiKey: {
|
||||
name: `${providerId} API key`,
|
||||
resolve: () => Promise.resolve(undefined),
|
||||
},
|
||||
},
|
||||
getModels: () => [],
|
||||
stream: () => { throw new Error("stream should not be called in this test"); },
|
||||
streamSimple: () => { throw new Error("streamSimple should not be called in this test"); },
|
||||
};
|
||||
}
|
||||
|
||||
describe("installGlobalProviderPolicy", () => {
|
||||
it("swallows registrations and records each rejection", async () => {
|
||||
it("rejects non-allowed registrations and records each rejection", async () => {
|
||||
const { runtime, rejections } = await policyRuntime();
|
||||
|
||||
runtime.registerProvider("acme", { baseUrl: "https://acme.example.com" });
|
||||
@@ -28,11 +47,37 @@ describe("installGlobalProviderPolicy", () => {
|
||||
expect(runtime.getRegisteredProviderConfig("acme")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("makes unregisterProvider a no-op that cannot remove global providers", async () => {
|
||||
const { runtime, rejections } = await policyRuntime();
|
||||
it("lets allowed (global-extension) providers through to the runtime", async () => {
|
||||
const { runtime, rejections } = await policyRuntime(new Set(["tensorx"]));
|
||||
|
||||
runtime.registerProvider("tensorx", { baseUrl: "https://tensorx.example.com" });
|
||||
runtime.registerProvider("acme", { baseUrl: "https://acme.example.com" });
|
||||
|
||||
expect(rejections).toEqual(["acme"]);
|
||||
expect(runtime.getRegisteredProviderIds()).toEqual(["tensorx"]);
|
||||
expect(runtime.getRegisteredProviderConfig("tensorx")).toEqual({ baseUrl: "https://tensorx.example.com" });
|
||||
});
|
||||
|
||||
it("applies the same allow rule to native provider registrations", async () => {
|
||||
const { runtime, rejections } = await policyRuntime(new Set(["native-global"]));
|
||||
|
||||
runtime.registerNativeProvider(nativeProvider("native-global"));
|
||||
runtime.registerNativeProvider(nativeProvider("native-project"));
|
||||
|
||||
expect(rejections).toEqual(["native-project"]);
|
||||
expect(runtime.getRegisteredProviderIds()).toEqual(["native-global"]);
|
||||
expect(runtime.getRegisteredNativeProvider("native-global")).toBeDefined();
|
||||
});
|
||||
|
||||
it("unregisters only allowed providers; other unregisters are a no-op", async () => {
|
||||
const { runtime, rejections } = await policyRuntime(new Set(["tensorx"]));
|
||||
runtime.registerProvider("tensorx", { baseUrl: "https://tensorx.example.com" });
|
||||
|
||||
runtime.unregisterProvider("acme");
|
||||
runtime.unregisterProvider(TEST_MODEL_PROVIDER);
|
||||
expect(runtime.getModel(TEST_MODEL_PROVIDER, TEST_MODEL_ID)).toBeDefined();
|
||||
|
||||
runtime.unregisterProvider("tensorx");
|
||||
|
||||
expect(rejections).toEqual([]);
|
||||
expect(runtime.getRegisteredProviderIds()).toEqual([]);
|
||||
@@ -56,7 +101,8 @@ describe("providerRejectionMessage", () => {
|
||||
|
||||
expect(message).toContain('Provider "acme"');
|
||||
expect(message).toContain("in /workspace/project");
|
||||
expect(message).toContain("PI WEB only supports globally configured providers");
|
||||
expect(message).toContain("PI WEB providers must come from global configuration");
|
||||
expect(message).toContain("globally installed extension");
|
||||
expect(message).toContain("All other extension features are unaffected.");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,38 +1,101 @@
|
||||
import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { createAgentSessionServices, type ModelRuntime } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
/**
|
||||
* PI WEB supports only globally configured providers: Pi built-ins, agent-dir
|
||||
* `models.json`, and environment credentials. Any provider an extension tries
|
||||
* to register is rejected — the user is told, and everything else the
|
||||
* extension does keeps working.
|
||||
* PI WEB providers come from global sources only: Pi built-ins, agent-dir
|
||||
* `models.json`, environment credentials, and providers registered by global
|
||||
* (agent-dir) extensions. Provider registrations from project extensions are
|
||||
* rejected — the user is told, and everything else the extension does keeps
|
||||
* working.
|
||||
*
|
||||
* Why: all sessions share one daemon-wide {@link ModelRuntime}. Letting one
|
||||
* workspace's extensions mutate it corrupts the provider set of every other
|
||||
* concurrent session (issue #76). Rather than building scoped-provider
|
||||
* isolation, pi-web rejects scoped registrations outright.
|
||||
* Why: all sessions share one daemon-wide {@link ModelRuntime}. Global
|
||||
* extensions are identical for every session, so their providers are
|
||||
* daemon-wide consistent; project extensions differ per workspace, and letting
|
||||
* them mutate the shared runtime corrupts the provider set of every other
|
||||
* concurrent session (issue #76).
|
||||
*
|
||||
* Mechanism: this deliberately shadows the `registerProvider` /
|
||||
* `unregisterProvider` instance methods because Pi 0.80.10 offers no
|
||||
* registration hook. Both Pi call sites (the load-time
|
||||
* `pendingProviderRegistrations` drain in `createAgentSessionServices` and the
|
||||
* late `pi.registerProvider` path through `ModelRegistry`) reach the runtime
|
||||
* through call-time property lookup, so instance shadowing intercepts them
|
||||
* identically. The acceptance test that exercises both paths is the tripwire:
|
||||
* if a Pi upgrade changes these internals, that test fails loudly and this
|
||||
* shim must be revisited.
|
||||
* `registerNativeProvider` / `unregisterProvider` instance methods because Pi
|
||||
* 0.81.1 offers no registration hook with extension attribution (the internal
|
||||
* drain, the bind-time flush, and the late `pi.registerProvider` path all
|
||||
* reach the runtime through call-time property lookup with the extension path
|
||||
* already dropped, so instance shadowing intercepts them identically). The
|
||||
* acceptance suite that exercises the load-time and late paths is the
|
||||
* tripwire: if a Pi upgrade changes these internals, those tests fail loudly
|
||||
* and this shim must be revisited.
|
||||
*
|
||||
* Attribution: Pi drops the registering extension's path before calls reach
|
||||
* the runtime, so the shim cannot tell global from project extensions per
|
||||
* call. Instead the daemon learns once, at startup, which provider ids global
|
||||
* extensions register ({@link learnGlobalExtensionProviderIds}) and allows
|
||||
* exactly those. This is not a security boundary — extensions run in-process
|
||||
* with full trust, and a project extension re-registering an allowed id would
|
||||
* pass. It is a guard against accidental cross-workspace leakage.
|
||||
*/
|
||||
export function installGlobalProviderPolicy(
|
||||
runtime: ModelRuntime,
|
||||
allowedExtensionProviderIds: ReadonlySet<string>,
|
||||
onRejection: (providerId: string) => void,
|
||||
): void {
|
||||
runtime.registerProvider = (providerId: string) => {
|
||||
const registerProvider = runtime.registerProvider.bind(runtime);
|
||||
const registerNativeProvider = runtime.registerNativeProvider.bind(runtime);
|
||||
const unregisterProvider = runtime.unregisterProvider.bind(runtime);
|
||||
|
||||
runtime.registerProvider = (providerId, config) => {
|
||||
if (allowedExtensionProviderIds.has(providerId)) {
|
||||
registerProvider(providerId, config);
|
||||
return;
|
||||
}
|
||||
// Swallow: the shared runtime is never mutated, the rejection is surfaced.
|
||||
onRejection(providerId);
|
||||
};
|
||||
runtime.unregisterProvider = () => {
|
||||
// No-op: with every registration rejected, the extension provider layer is
|
||||
// always empty, so there is never anything to unregister.
|
||||
runtime.registerNativeProvider = (provider) => {
|
||||
if (allowedExtensionProviderIds.has(provider.id)) {
|
||||
registerNativeProvider(provider);
|
||||
return;
|
||||
}
|
||||
onRejection(provider.id);
|
||||
};
|
||||
runtime.unregisterProvider = (providerId) => {
|
||||
if (allowedExtensionProviderIds.has(providerId)) {
|
||||
unregisterProvider(providerId);
|
||||
return;
|
||||
}
|
||||
// No-op: rejected registrations never reached the runtime, so there is
|
||||
// never anything of theirs to unregister.
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Learn which provider ids the agent directory's global extensions register.
|
||||
*
|
||||
* Loads extensions once for a guaranteed-empty temporary cwd — so only global
|
||||
* (agent-dir) extensions load — against the daemon's shared runtime, and
|
||||
* returns the ids that appeared. Those registrations stay on the runtime: they
|
||||
* are the daemon baseline, re-registered identically on every session load.
|
||||
* Must run before {@link installGlobalProviderPolicy} is installed.
|
||||
*
|
||||
* Caveat: global extension code runs one extra time at daemon startup. Any
|
||||
* commands/tools it registers land on the discarded scratch loader.
|
||||
*/
|
||||
export async function learnGlobalExtensionProviderIds(
|
||||
runtime: ModelRuntime,
|
||||
agentDir: string,
|
||||
): Promise<ReadonlySet<string>> {
|
||||
const before = new Set(runtime.getRegisteredProviderIds());
|
||||
const scratchCwd = await mkdtemp(join(tmpdir(), "pi-web-global-ext-"));
|
||||
try {
|
||||
await createAgentSessionServices({ cwd: scratchCwd, agentDir, modelRuntime: runtime });
|
||||
} finally {
|
||||
await rm(scratchCwd, { recursive: true, force: true });
|
||||
}
|
||||
const learned = new Set<string>();
|
||||
for (const providerId of runtime.getRegisteredProviderIds()) {
|
||||
if (!before.has(providerId)) learned.add(providerId);
|
||||
}
|
||||
return learned;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,6 +105,6 @@ export function installGlobalProviderPolicy(
|
||||
*/
|
||||
export function providerRejectionMessage(providerId: string, cwd?: string): string {
|
||||
const origin = cwd === undefined ? "registered by an extension" : `registered by an extension in ${cwd}`;
|
||||
return `Provider "${providerId}" ${origin} was ignored — PI WEB only supports globally configured providers. `
|
||||
+ "Configure it globally (e.g. agent-dir models.json) to use it here. All other extension features are unaffected.";
|
||||
return `Provider "${providerId}" ${origin} was ignored — PI WEB providers must come from global configuration `
|
||||
+ "(agent-dir models.json) or a globally installed extension. All other extension features are unaffected.";
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ describe("PiSessionService prompt, queue, and auth warnings", () => {
|
||||
return stream;
|
||||
};
|
||||
const hub = new CapturingSessionEventHub();
|
||||
const fake = fakeRuntime("name-session", { model, agent: { streamFn } });
|
||||
const fake = fakeRuntime("name-session", { model, agent: { streamFunction: streamFn } });
|
||||
const service = new PiSessionService(hub, {
|
||||
agentDir: TEST_AGENT_DIR,
|
||||
modelRuntime: testModelRuntime,
|
||||
|
||||
@@ -208,7 +208,7 @@ export function fakeRuntime(sessionId = "session-1", patch: Partial<TestSession>
|
||||
setSessionName: (name: string) => { session.sessionName = name; },
|
||||
compact: () => Promise.resolve({ summary: "", tokensBefore: 0 }),
|
||||
getUserMessagesForForking: () => [],
|
||||
agent: { streamFn: () => { throw new Error("streamFn should not be called in this test"); } },
|
||||
agent: { streamFunction: () => { throw new Error("streamFunction should not be called in this test"); } },
|
||||
...patch,
|
||||
};
|
||||
const runtime: PiSessionRuntime = {
|
||||
|
||||
@@ -346,13 +346,13 @@ export interface PiAgentSession {
|
||||
setSessionName(name: string): void;
|
||||
/**
|
||||
* Narrow re-expression of `AgentSession.agent` (an `@earendil-works/pi-agent-core`
|
||||
* `Agent`), exposing only `streamFn` — the resolved-auth/headers/retry "call this
|
||||
* model" function pi's own compaction/branch-summarization code uses internally.
|
||||
* Lets callers (e.g. session title generation) issue one-off model calls without
|
||||
* depending on pi-ai's deprecated `/compat` provider registry or leaking the full
|
||||
* `Agent`/`AgentSession` surface.
|
||||
* `Agent`), exposing only `streamFunction` — the resolved-auth/headers/retry "call
|
||||
* this model" function pi's own compaction/branch-summarization code uses
|
||||
* internally. Lets callers (e.g. session title generation) issue one-off model
|
||||
* calls without depending on pi-ai's deprecated `/compat` provider registry or
|
||||
* leaking the full `Agent`/`AgentSession` surface.
|
||||
*/
|
||||
agent: { streamFn: StreamFn };
|
||||
agent: { streamFunction: StreamFn };
|
||||
}
|
||||
|
||||
export interface PiSessionRuntime {
|
||||
@@ -2761,7 +2761,7 @@ export class PiSessionService implements SessionRouteService {
|
||||
const model = session.model;
|
||||
if (model === undefined) return;
|
||||
|
||||
void generateShortSessionName(session.agent.streamFn, model, firstMessage).then((name) => {
|
||||
void generateShortSessionName(session.agent.streamFunction, model, firstMessage).then((name) => {
|
||||
this.applyGeneratedSessionName(session, name ?? fallbackSessionName(firstMessage));
|
||||
}).catch(() => {
|
||||
this.applyGeneratedSessionName(session, fallbackSessionName(firstMessage));
|
||||
|
||||
Reference in New Issue
Block a user