Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/host-daemon/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
"@bb/templates": "workspace:*",
"@bb/tunnel-client": "workspace:*",
"@bb/tunnel-contract": "workspace:*",
"@earendil-works/pi-agent-core": "0.84.0",
"@earendil-works/pi-ai": "0.84.0",
"@earendil-works/pi-coding-agent": "0.84.0",
"@earendil-works/pi-tui": "0.84.0",
"@hono/node-server": "^1.19.11",
"@parcel/watcher": "2.5.6",
"better-sqlite3": "12.10.0",
Expand Down
10 changes: 7 additions & 3 deletions apps/host-daemon/scripts/build-bundles.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { build } from "esbuild";
import { bundleTargets } from "./bundle-manifest.mjs";
import {
createNativeExternalPatterns,
externalPackagePatterns,
generateTemplatesIfRequested,
} from "../../../scripts/build-utils.mjs";

Expand All @@ -25,9 +26,12 @@ async function main() {
bundle: true,
conditions: ["source"],
entryPoints: [target.entryPoint],
external: createNativeExternalPatterns({
bundledPackages: target.bundledPackages,
}),
external: [
...createNativeExternalPatterns({
bundledPackages: target.bundledPackages,
}),
...externalPackagePatterns(target.externalPackages ?? []),
],
format: "esm",
legalComments: "none",
minify: true,
Expand Down
17 changes: 6 additions & 11 deletions apps/host-daemon/scripts/bundle-manifest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export const bundleTargets = [
},
{
banner: NODE_ESM_REQUIRE_BANNER,
bundledPackages: ["jiti"],
entryPoint: resolve(
workspaceRoot,
"packages",
Expand All @@ -47,18 +46,14 @@ export const bundleTargets = [
"bridge",
"bridge.ts",
),
// Pi extensions import the host's Pi modules. Keep the pinned Pi package
// tree on disk so Pi's extension loader can resolve those shared modules.
externalPackages: [
"@earendil-works/pi-ai",
"@earendil-works/pi-coding-agent",
],
label: "pi bridge",
outfile: resolve(packageRoot, "dist", "bb-pi-bridge.mjs"),
// The bridge ships as one file, so Pi's OAuth flows cannot be reached
// through its normal relative dynamic imports; `registerBunOAuthFlows()`
// pulls them in statically instead. A rename or removal upstream would
// otherwise surface only at login time inside the packaged bridge, so
// assert the flows really landed in the bundle. These are endpoint
// literals, which survive minification.
requiredLiterals: [
"https://claude.ai/oauth/authorize",
"https://auth.openai.com",
],
},
{
banner: NODE_ESM_REQUIRE_BANNER,
Expand Down
1 change: 1 addition & 0 deletions apps/host-daemon/src/command-dispatch-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface CommandDispatchOptions {
listModels?: (args: {
providerId: string;
acpLaunchSpec?: HostDaemonAcpLaunchSpec;
cwd?: string;
}) => Promise<{
models: AvailableModel[];
selectedOnlyModels: AvailableModel[];
Expand Down
1 change: 1 addition & 0 deletions apps/host-daemon/src/command-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ const onlineRpcHandlers: OnlineRpcHandlerMap = {
"provider.list_models": async (command, options) =>
(options.listModels ?? defaultListModels)({
providerId: command.providerId,
...(command.cwd !== undefined ? { cwd: command.cwd } : {}),
...(command.acpLaunchSpec !== undefined
? { acpLaunchSpec: command.acpLaunchSpec }
: {}),
Expand Down
8 changes: 7 additions & 1 deletion apps/host-daemon/test/command/thread-dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1767,14 +1767,19 @@ describe("thread command dispatch", () => {
const harness = createHarness();
const acpLaunchSpec = customAcpLaunchSpec();
let capturedListModelsArgs:
| { providerId: string; acpLaunchSpec?: HostDaemonAcpLaunchSpec }
| {
providerId: string;
acpLaunchSpec?: HostDaemonAcpLaunchSpec;
cwd?: string;
}
| undefined;

const result = await dispatchOnlineRpcCommand(
{
type: "provider.list_models",
providerId: "fake",
acpLaunchSpec,
cwd: "/tmp/worktree",
},
{
...harness.dispatchOptions(),
Expand Down Expand Up @@ -1811,6 +1816,7 @@ describe("thread command dispatch", () => {
expect(capturedListModelsArgs).toEqual({
providerId: "fake",
acpLaunchSpec,
cwd: "/tmp/worktree",
});
expect(result).toEqual({
models: [
Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@better-auth/drizzle-adapter": "^1.6.23",
"@hono/node-server": "^1.19.11",
"@hono/node-ws": "^1.3.0",
"@earendil-works/pi-ai": "^0.82.0",
"@earendil-works/pi-ai": "0.84.0",
"@opentelemetry/api": "1.9.1",
"better-auth": "^1.6.23",
"better-sqlite3": "12.10.0",
Expand Down
12 changes: 12 additions & 0 deletions apps/server/src/services/system/execution-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { COMMAND_TIMEOUT_MS } from "../../constants.js";
import { ApiError } from "../../errors.js";
import { callHostRetryableOnlineRpc } from "../hosts/online-rpc.js";
import { getHostPermissionCeiling } from "../hosts/permission-ceiling.js";
import { requireEnvironment } from "../lib/entity-lookup.js";
import { getSupportedReasoningLevelsForProvider } from "../threads/thread-reasoning-policy.js";
import { resolveSystemLookupHostId } from "./host-lookup.js";
import {
Expand All @@ -43,6 +44,7 @@ interface BuildModelLoadErrorArgs {
}

export interface ResolveSystemProviderModelsArgs {
cwd?: string;
hostId: string;
providerId: string;
}
Expand Down Expand Up @@ -303,6 +305,7 @@ export async function resolveSystemProviderModels(
}

const result = await loadSystemProviderModels(deps, {
...(args.cwd !== undefined ? { cwd: args.cwd } : {}),
hostId: args.hostId,
provider,
});
Expand Down Expand Up @@ -393,6 +396,10 @@ export async function resolveSystemExecutionOptions(
deps: LoggedWorkSessionDeps,
query: SystemExecutionOptionsRequest,
): Promise<SystemExecutionOptionsResponse> {
const cwd =
query.environmentId === undefined
? undefined
: (requireEnvironment(deps.db, query.environmentId).path ?? undefined);
const { hostId, hostLookupError, providersPromise } =
resolveSystemProviderInfosPlan(deps, query);
const configuredRequestedProvider = query.providerId
Expand All @@ -403,6 +410,7 @@ export async function resolveSystemExecutionOptions(
const earlyModelResultPromise =
hostId !== null && configuredRequestedProvider
? loadSystemProviderModels(deps, {
...(cwd !== undefined ? { cwd } : {}),
hostId,
provider: configuredRequestedProvider,
})
Expand Down Expand Up @@ -461,6 +469,7 @@ export async function resolveSystemExecutionOptions(
earlyModelResultPromise !== null
? await earlyModelResultPromise
: await loadSystemProviderModels(deps, {
...(cwd !== undefined ? { cwd } : {}),
hostId,
provider: modelsProvider,
});
Expand All @@ -484,9 +493,11 @@ export async function resolveSystemExecutionOptions(
async function loadSystemProviderModels(
deps: LoggedWorkSessionDeps,
{
cwd,
hostId,
provider,
}: {
cwd?: string;
hostId: string;
provider: ProviderInfo;
},
Expand All @@ -508,6 +519,7 @@ async function loadSystemProviderModels(
command: {
type: "provider.list_models",
providerId: provider.id,
...(cwd !== undefined ? { cwd } : {}),
...(customAcpAgent !== undefined
? {
acpLaunchSpec: normalizeHostDaemonAcpLaunchSpec(customAcpAgent),
Expand Down
22 changes: 22 additions & 0 deletions apps/server/src/services/threads/thread-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ interface DeriveThreadCreateTitleFallbackArgs {
}

interface ResolveCatalogExecutionDefaultsArgs {
cwd?: string;
executionDefaults: ProjectExecutionDefaults | null;
hostId: string | null;
providerId: string;
Expand All @@ -131,6 +132,7 @@ async function resolveCatalogExecutionDefaults(
}

const catalog = await resolveSystemProviderModels(deps, {
...(args.cwd !== undefined ? { cwd: args.cwd } : {}),
hostId: args.hostId,
providerId: args.providerId,
});
Expand Down Expand Up @@ -221,6 +223,23 @@ function childHostIdForResolvedEnvironment(
}
}

function modelCatalogCwdForResolvedEnvironment(
resolvedEnvironment: ResolvedStableThreadRequestEnvironment,
): string | undefined {
switch (resolvedEnvironment.type) {
case "reuse":
return resolvedEnvironment.environment.path ?? undefined;
case "host":
return (
resolvedEnvironment.unmanagedPath ??
resolvedEnvironment.localSource?.path ??
undefined
);
case "personal":
return undefined;
}
}

function deriveThreadCreateTitleFallback({
input,
originKind,
Expand Down Expand Up @@ -743,9 +762,12 @@ export async function createThreadFromRequest(
const hostDataDir = await ensureCreateHostOnline(deps, {
resolvedEnvironment,
});
const modelCatalogCwd =
modelCatalogCwdForResolvedEnvironment(resolvedEnvironment);
const resolvedExecutionDefaults = await resolveCatalogExecutionDefaults(
deps,
{
...(modelCatalogCwd !== undefined ? { cwd: modelCatalogCwd } : {}),
executionDefaults,
hostId: childHostIdForResolvedEnvironment(resolvedEnvironment),
providerId,
Expand Down
6 changes: 5 additions & 1 deletion apps/server/test/public/public-thread-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3218,7 +3218,11 @@ describe("public thread data routes", () => {
{ id: "acp-hermes-agent", executableName: "hermes" },
],
},
{ type: "provider.list_models", providerId: "codex" },
{
type: "provider.list_models",
providerId: "codex",
cwd: "/tmp/test-environment",
},
]);
});
});
Expand Down
8 changes: 7 additions & 1 deletion apps/server/test/public/public-threads.defaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,13 @@ describe("public thread default routes", () => {
});
expect(
providerResponder.requests.map((request) => request.command),
).toEqual([{ type: "provider.list_models", providerId: "codex" }]);
).toEqual([
{
type: "provider.list_models",
providerId: "codex",
cwd: "/tmp/thread-defaults-missing",
},
]);
});
});

Expand Down
18 changes: 16 additions & 2 deletions apps/server/test/system/provider-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ describe("system provider host routing", () => {
const remote = seedHostSession(harness.deps, {
id: "host-provider-remote",
});
const remoteModelCommands: HostDaemonOnlineRpcRequestMessage["command"][] =
[];
seedPrimaryHost(harness.deps, primary.host.id);

registerHostRpcResponder(harness, {
Expand All @@ -71,8 +73,12 @@ describe("system provider host routing", () => {
registerHostRpcResponder(harness, {
hostId: remote.host.id,
sessionId: remote.session.id,
handle: (request) =>
providerHostResponse(request, "acp-omp", "remote-model"),
handle: (request) => {
if (request.command.type === "provider.list_models") {
remoteModelCommands.push(request.command);
}
return providerHostResponse(request, "acp-omp", "remote-model");
},
});

const { project } = seedProjectWithSource(harness.deps, {
Expand Down Expand Up @@ -137,6 +143,14 @@ describe("system provider host routing", () => {
expect(environmentModels.models.map((model) => model.model)).toEqual([
"remote-model",
]);
expect(remoteModelCommands).toEqual([
{ type: "provider.list_models", providerId: "codex" },
{
type: "provider.list_models",
providerId: "codex",
cwd: "/tmp/test-environment",
},
]);
});
});

Expand Down
1 change: 1 addition & 0 deletions docs/repository-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ visible from `package.json` alone.
| Dependency | Where | Why |
| ------------------------------ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@opentelemetry/api` (`1.9.1`) | `apps/server` | Pi AI and Drizzle each pull in `@opentelemetry/api`. Without an exact direct pin, pnpm can resolve two copies and TypeScript sees two distinct type identities, which fails the server typecheck. Bump both consumers together, not this pin alone. |
| Pi packages (`0.84.0`) | Pi bridge and `bb-app` | Pi extensions import the host's Pi modules. The packaged bridge keeps this exact package tree on disk so extensions share one compatible runtime. Bump the Pi packages together. |
4 changes: 2 additions & 2 deletions packages/agent-runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
"@bb/host-daemon-contract": "workspace:*",
"@bb/process-utils": "workspace:*",
"@modelcontextprotocol/sdk": "^1.29.0",
"@earendil-works/pi-ai": "^0.82.0",
"@earendil-works/pi-coding-agent": "^0.82.0",
"@earendil-works/pi-ai": "0.84.0",
"@earendil-works/pi-coding-agent": "0.84.0",
"zod": "^4.3.6"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-runtime/src/pi/adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1115,7 +1115,7 @@ describe("pi provider adapter", () => {
});

it("translateEvent drops agent_settled instead of surfacing it in the transcript", () => {
// Pi 0.82 emits agent_settled after every agent run. Without an explicit
// Pi emits agent_settled after every agent run. Without an explicit
// ignore it falls through to provider/unhandled, which renders as
// "Unhandled Pi event" in the thread for the user on every single turn.
const adapter = createPiProviderAdapter();
Expand Down
4 changes: 2 additions & 2 deletions packages/agent-runtime/src/pi/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ const piEventTypeSchema = z
// fallback treats them as unknown and emits a `provider/unhandled` event, which
// renders as "Unhandled Pi event" in the transcript.
//
// `agent_settled` fires after every agent run completes (Pi 0.82's
// `agent_settled` fires after every agent run completes (Pi's
// AgentSession._emitAgentSettled). BB already derives turn completion from
// `agent_end` plus its `willRetry` flag, so the settle signal carries nothing
// extra for us.
Expand Down Expand Up @@ -1277,7 +1277,7 @@ export function createPiProviderAdapter(
return {
kind: "request",
method: "model/list",
params: {},
params: command.cwd ? { cwd: command.cwd } : {},
};
case "skills/configure":
return {
Expand Down
Loading
Loading