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
90 changes: 81 additions & 9 deletions ts/packages/agentServer/client/src/agentServerClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,18 +557,90 @@ export async function connectAgentServer(
);
}

// Candidate locations for the production agent-server install, matching the
// layout install-typeagent.ps1 / install-typeagent.sh lay down (a deployed
// artifact dir whose root holds the typeagent-serve.mjs launcher). We target
// the launcher rather than dist/server.js because a deployed artifact can be
// profile-pruned: the launcher reads the .typeagent-profile marker and starts
// the daemon with the matching --config, which the pruned artifact needs to
// load its agents. Kept in sync with those installers' InstallDir.
function getProductionServerCandidates(): string[] {
const candidates: string[] = [];
const server = (dir: string) => path.join(dir, "typeagent-serve.mjs");
if (process.platform === "win32") {
const localAppData =
process.env.LOCALAPPDATA ??
(process.env.USERPROFILE
? path.join(process.env.USERPROFILE, "AppData", "Local")
: undefined);
if (localAppData) {
candidates.push(
server(path.join(localAppData, "TypeAgent", "agent-server")),
);
}
} else if (process.platform === "darwin") {
candidates.push(
server(
path.join(
os.homedir(),
"Library",
"Application Support",
"TypeAgent",
"agent-server",
),
),
);
} else {
const xdg =
process.env.XDG_DATA_HOME ??
path.join(os.homedir(), ".local", "share");
candidates.push(server(path.join(xdg, "typeagent", "agent-server")));
}
return candidates;
}

// Locate the agent-server entry point to spawn. Resolution order:
// 1. TYPEAGENT_SERVER_PATH env override (explicit full path to the entry to
// run with node — a deployed typeagent-serve.mjs or a dist/server.js).
// 2. The production install dir used by the installers (per-platform): the
// deployed typeagent-serve.mjs launcher.
// 3. The local workspace build, relative to this client package
// (client/dist -> server/dist/server.js), for repo/dev use.
// Returns the first that exists. This lets a client (shell, CLI, plugin) that no
// longer bundles the agent-server still find a separately installed one. The
// spawn path runs `node <entry> --port <n> [--idle-timeout <m>]`, which both
// the launcher (defaults to its `start` command) and dist/server.js accept.
function getAgentServerEntryPoint(): string {
const probed: string[] = [];

const envOverride = process.env.TYPEAGENT_SERVER_PATH;
if (envOverride) {
probed.push(`${envOverride} (TYPEAGENT_SERVER_PATH)`);
if (fs.existsSync(envOverride)) {
return envOverride;
}
}

for (const candidate of getProductionServerCandidates()) {
probed.push(`${candidate} (install dir)`);
if (fs.existsSync(candidate)) {
return candidate;
}
}

const thisDir = path.dirname(fileURLToPath(import.meta.url));
// From client/dist/ -> server/dist/server.js
const serverPath = path.resolve(thisDir, "../../server/dist/server.js");
if (!fs.existsSync(serverPath)) {
throw new Error(
`Agent server entry point not found at ${serverPath}. ` +
`The expected relative path from the client package may have changed. ` +
`Ensure the agent-server package is built.`,
);
const workspacePath = path.resolve(thisDir, "../../server/dist/server.js");
probed.push(`${workspacePath} (workspace)`);
if (fs.existsSync(workspacePath)) {
return workspacePath;
}
return serverPath;

throw new Error(
`Agent server entry point not found. Probed:\n ${probed.join("\n ")}\n` +
`Install the TypeAgent agent service, set TYPEAGENT_SERVER_PATH to the ` +
`agent-server's dist/server.js, or build the agent-server package.`,
);
}

export function isServerRunning(url: string): Promise<boolean> {
Expand Down Expand Up @@ -758,7 +830,7 @@ function spawnAgentServer(

async function waitForServer(
url: string,
timeoutMs: number = 60000,
timeoutMs: number = 120000,
pollIntervalMs: number = 500,
): Promise<void> {
const start = Date.now();
Expand Down
4 changes: 4 additions & 0 deletions ts/packages/agentServer/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ async function main() {
},
collectCommandResult: true,
portRegistrar,
// Grant the browser agent permission to read other agents'
// local-view ports so inline-browser embedding works in
// connect mode, matching the standalone (in-process) shell.
allowSharedLocalView: ["browser"],
},
instanceDir,
);
Expand Down
71 changes: 47 additions & 24 deletions ts/packages/agents/browser/src/agent/browserActionHandler.mts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ import {
defaultSearchProviders,
} from "../common/browserControl.mjs";
import { openai } from "@typeagent/aiclient";
import { urlResolver } from "azure-ai-foundry";
import {
SearchProviderCommandHandlerTable,
SetCommandHandler,
Expand Down Expand Up @@ -516,8 +515,15 @@ async function initializeBrowserContext(
sessionId: "default",
clientBrowserControl,
useExternalBrowserControl: clientBrowserControl === undefined,
// With no in-process control (connect mode, or extension-only), leave
// the preferred client type unset so selectActiveClientForSession uses
// its default priority (electron > extension > any). This lets the
// shell's inline browser (an "electron" client, clientId
// "inlineBrowser") drive control in connect mode while still selecting
// the extension when it's the only client. Standalone keeps "electron"
// because it provides the control in-process.
preferredClientType:
clientBrowserControl === undefined ? "extension" : "electron",
clientBrowserControl === undefined ? undefined : "electron",
index: undefined,
localHostPort,
// Shared WebSocket server is created lazily on the first
Expand Down Expand Up @@ -595,31 +601,47 @@ async function updateBrowserContext(
}

if (!context.agentContext.viewProcess) {
const viewProcess = await createViewServiceHost(context);
if (viewProcess) {
context.agentContext.viewProcess = viewProcess;
// Defensive cleanup if the child crashes mid-session.
// The dispatcher's PortRegistrar leaves stale entries
// bounded to "until respawn or session end", but a
// crashed child should release its registration eagerly
// so the entry doesn't shadow a fresh bind. The
// identity guard prevents a late-firing `exit` event on
// a previously-replaced process from clobbering a newer
// registration; the explicit disable path (which also
// releases) is naturally idempotent under `?.release()`.
viewProcess.once("exit", () => {
if (context.agentContext.viewProcess !== viewProcess) {
// Fork the express view service (static file host) in the
// background rather than blocking agent enable — and therefore
// agent-server startup — on it. Nothing on the enable path awaits
// the process object; its port is registered independently inside
// createViewServiceHost when the child reports ready. This keeps a
// slow/cold view-service fork (up to the 10s timeout) off the
// launch critical path.
void createViewServiceHost(context)
.then((viewProcess) => {
if (!viewProcess) {
return;
}
context.agentContext.viewPortRegistration?.release();
context.agentContext.viewPortRegistration = undefined;
context.agentContext.viewProcess = undefined;
// Reset cached port so respawn forks with arg "0"
// (OS-assigned) instead of trying to re-bind the
// stale port — mirrors the disable/close paths.
context.agentContext.localHostPort = 0;
context.agentContext.viewProcess = viewProcess;
// Defensive cleanup if the child crashes mid-session.
// The dispatcher's PortRegistrar leaves stale entries
// bounded to "until respawn or session end", but a
// crashed child should release its registration eagerly
// so the entry doesn't shadow a fresh bind. The
// identity guard prevents a late-firing `exit` event on
// a previously-replaced process from clobbering a newer
// registration; the explicit disable path (which also
// releases) is naturally idempotent under `?.release()`.
viewProcess.once("exit", () => {
if (context.agentContext.viewProcess !== viewProcess) {
return;
}
context.agentContext.viewPortRegistration?.release();
context.agentContext.viewPortRegistration = undefined;
context.agentContext.viewProcess = undefined;
// Reset cached port so respawn forks with arg "0"
// (OS-assigned) instead of trying to re-bind the
// stale port — mirrors the disable/close paths.
context.agentContext.localHostPort = 0;
});
})
.catch((e) => {
debug(
"Browser view service background start failed:",
e?.message ?? e,
);
});
}
}

if (context.agentContext.browserSchemaEnabled) {
Expand Down Expand Up @@ -1376,6 +1398,7 @@ async function resolveWebPage(
context.agentContext.resolverSettings.keywordResolver ||
fastResolution
) {
const { urlResolver } = await import("azure-ai-foundry");
const cachehitUrls = urlResolver.resolveURLByKeyword(site);
if (cachehitUrls && cachehitUrls.length > 0) {
debug(`Resolved URLs from cache: ${cachehitUrls}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ export function createExternalBrowserClient(
sessionId: string,
): ExternalBrowserClient {
function getActiveRpc() {
const client = agentWebSocketServer.getActiveClient(
sessionId,
"extension",
);
const client = agentWebSocketServer.getActiveClient(sessionId);
if (!client?.browserControlRpc) {
throw new Error("No browser control connection available");
}
Expand Down
63 changes: 43 additions & 20 deletions ts/packages/agents/markdown/src/agent/markdownActionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,32 +304,55 @@ async function updateMarkdownContext(
const fullPath = await getFullMarkdownFilePath(fileName, storage!);
if (fullPath) {
process.env.MARKDOWN_FILE = fullPath;
const result = await createViewServiceHost(
// Fork the express view service in the background instead of
// blocking agent enable (and therefore agent-server startup)
// on it. The view is only needed once the user actually opens
// the markdown view; every action handler guards on
// `viewProcess` presence, so early actions simply skip the
// view until it's ready. This keeps a slow/cold view-service
// fork (up to the 10s timeout) off the launch critical path.
void createViewServiceHost(
fullPath,
context.agentContext.localHostPort,
);
if (result) {
const viewProcess = result.process;
context.agentContext.viewProcess = viewProcess;
context.agentContext.localHostPort = result.port;
context.agentContext.viewPortRegistration?.release();
context.agentContext.viewPortRegistration =
context.registerPort("view", result.port);
// Defensive cleanup if the child crashes mid-session.
// The identity guard prevents a late-firing `exit`
// event on a previously-replaced process from
// clobbering a newer registration; the explicit
// disable path (which also releases) is naturally
// idempotent under `?.release()`.
viewProcess.once("exit", () => {
if (context.agentContext.viewProcess !== viewProcess) {
)
.then((result) => {
if (!result) {
return;
}
const viewProcess = result.process;
context.agentContext.viewProcess = viewProcess;
context.agentContext.localHostPort = result.port;
context.agentContext.viewPortRegistration?.release();
context.agentContext.viewPortRegistration = undefined;
context.agentContext.viewProcess = undefined;
context.agentContext.viewPortRegistration =
context.registerPort("view", result.port);
// Defensive cleanup if the child crashes mid-session.
// The identity guard prevents a late-firing `exit`
// event on a previously-replaced process from
// clobbering a newer registration; the explicit
// disable path (which also releases) is naturally
// idempotent under `?.release()`.
viewProcess.once("exit", () => {
if (
context.agentContext.viewProcess !== viewProcess
) {
return;
}
context.agentContext.viewPortRegistration?.release();
context.agentContext.viewPortRegistration =
undefined;
context.agentContext.viewProcess = undefined;
});
// Re-wire the UI-command message handler now that the
// view process exists (the earlier call below ran
// before it was forked).
setCurrentAgentContext(context.agentContext);
})
.catch((e) => {
console.warn(
"[AGENT] Markdown view service background start failed:",
e?.message ?? e,
);
});
}
}
}

Expand Down
4 changes: 3 additions & 1 deletion ts/packages/aiclient/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
"dependencies": {
"@azure/identity": "^4.10.0",
"@github/copilot-sdk": "1.0.5",
"@huggingface/transformers": "^3.8.1",
"@typeagent/common-utils": "workspace:*",
"@typeagent/config": "workspace:*",
"async": "^3.2.5",
Expand All @@ -51,6 +50,9 @@
"rimraf": "^6.0.1",
"typescript": "~5.4.5"
},
"optionalDependencies": {
"@huggingface/transformers": "^3.8.1"
},
"engines": {
"node": ">=22"
}
Expand Down
17 changes: 16 additions & 1 deletion ts/packages/shell/electron-builder.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@

// Configuration used for 'electron-builder build' step, and not 'install-app-deps' step.

import path from "node:path";
import { fileURLToPath } from "node:url";

// The Electron browser extension ships as a resource. Its source
// (browser-typeagent/dist/electron) is resolved from the shell package's own
// node_modules rather than the deploy tree: browser-typeagent is a
// devDependency (build/package-time only), so it is intentionally excluded
// from `pnpm deploy --prod` and its (large) runtime closure never lands in the
// packaged app. Only the prebuilt extension assets are copied.
const shellDir = path.dirname(fileURLToPath(import.meta.url));
const browserExtensionDir = path.join(
shellDir,
"node_modules/browser-typeagent/dist/electron",
);

const name = "typeagentshell";
const fullName = "TypeAgent Shell";
const account = process.env.AZURESTORAGEACCOUNTNAME;
Expand Down Expand Up @@ -55,7 +70,7 @@ export default {
// electron can't load the browser extension from the ASAR
extraResources: [
{
from: "node_modules/browser-typeagent/dist/electron",
from: browserExtensionDir,
to: "browser-typeagent-extension",
},
],
Expand Down
11 changes: 10 additions & 1 deletion ts/packages/shell/electron.vite.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,16 @@ function copySchemaPlugin() {

export default defineConfig({
main: {
plugins: [externalizeDepsPlugin(), copySchemaPlugin()],
plugins: [
externalizeDepsPlugin({
include: [
"agent-server",
"default-agent-provider",
"dispatcher-node-providers",
],
}),
copySchemaPlugin(),
],
build: {
sourcemap: true,
},
Expand Down
Loading
Loading