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
9 changes: 9 additions & 0 deletions ts/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,14 @@
"jest.jestCommandLine": "node --no-warnings --experimental-vm-modules ./node_modules/jest/bin/jest.js",
"chat.tools.terminal.autoApprove": {
"ForEach-Object": true
},
"files.watcherExclude": {
"**/node_modules/**": true,
"**/.git/objects/**": true,
"**/dist/**": true
},
"search.exclude": {
"**/node_modules": true,
"**/dist": true
}
}
6 changes: 6 additions & 0 deletions ts/docs/architecture/dispatcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,12 @@ provider.getAppAgentNames() # Discover available agents
│ │ - Load flow definitions │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ If manifest.localView = true: │
│ - Reserve a port slot (assigned 0 = OS-chosen) │
│ - Agent's view server spawned on first activation │
│ - Server binds to OS-assigned port, reports back │
│ via IPC → stored via SessionContext.setLocalHostPort│
│ │
└───────────────────────────────────────────────────────┘
```

Expand Down
7 changes: 7 additions & 0 deletions ts/packages/agentRpc/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,13 @@ export async function createAgentRpcClient(
const context = contextMap.get(param.contextId);
return context.getSharedLocalHostPort(param.agentName);
},
setLocalHostPort: async (param: {
contextId: number;
port: number;
}) => {
const context = contextMap.get(param.contextId);
context.setLocalHostPort(param.port);
},
indexes: async (param: { contextId: number; type: string }) => {
const context = contextMap.get(param.contextId);
return context.indexes(param.type as any);
Expand Down
5 changes: 5 additions & 0 deletions ts/packages/agentRpc/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,11 @@ export function createAgentRpcServer(
agentName,
});
},
setLocalHostPort(port: number) {
void rpc
.invoke("setLocalHostPort", { contextId, port })
.catch();
},
addDynamicAgent: async (
name: string,
manifest: AppAgentManifest,
Expand Down
4 changes: 4 additions & 0 deletions ts/packages/agentRpc/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ export type AgentContextInvokeFunctions = {
contextId: number;
agentName: string;
}) => Promise<number>;
setLocalHostPort: (param: {
contextId: number;
port: number;
}) => Promise<void>;
indexes: (param: { contextId: number; type: string }) => Promise<any>;
reloadAgentSchema: (param: { contextId: number }) => Promise<void>;
popupQuestion: (param: {
Expand Down
3 changes: 3 additions & 0 deletions ts/packages/agentSdk/src/agentInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@ export interface SessionContext<T = unknown> {
// Experimental: get the shared local host port
getSharedLocalHostPort(agentName: string): Promise<number>;

// Experimental: update this agent's bound local host port (used after OS port assignment)
setLocalHostPort(port: number): void;

// Experimental: get the available indexes
indexes(type: "image" | "email" | "website" | "all"): Promise<any[]>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ async function main() {
agents: { actions: true, commands: true },
execution: { history: false },
collectCommandResult: true,
portBase: 9400,
persistDir,
storageProvider: getFsStorageProvider(),
});
Expand Down
24 changes: 22 additions & 2 deletions ts/packages/agents/browser/src/agent/browserActionHandler.mts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ export function instantiate(): AppAgent {
return {
initializeAgentContext: initializeBrowserContext,
updateAgentContext: updateBrowserContext,
closeAgentContext: closeBrowserContext,
executeAction: executeBrowserAction,
resolveEntity,
getDynamicDisplay: getDynamicDisplayImpl,
Expand Down Expand Up @@ -540,6 +541,23 @@ async function updateBrowserContext(
}
}

async function closeBrowserContext(
context: SessionContext<BrowserActionContext>,
) {
if (context.agentContext.agentWebSocketServer) {
context.agentContext.agentWebSocketServer.stop();
delete context.agentContext.agentWebSocketServer;
}
if (context.agentContext.browserProcess) {
context.agentContext.browserProcess.kill();
context.agentContext.browserProcess = undefined;
}
if (context.agentContext.viewProcess) {
context.agentContext.viewProcess.kill();
context.agentContext.viewProcess = undefined;
}
}

async function handleWebAgentRpc(
method: string,
params: any,
Expand Down Expand Up @@ -1966,8 +1984,10 @@ async function createViewServiceHost(
},
});

childProcess.on("message", function (message) {
if (message === "Success") {
childProcess.on("message", function (message: any) {
if (message?.type === "Success") {
context.agentContext.localHostPort = message.port;
context.setLocalHostPort(message.port);
resolve(childProcess);
} else if (message === "Failure") {
resolve(undefined);
Expand Down
1 change: 1 addition & 0 deletions ts/packages/agents/browser/src/agent/websiteMemory.mts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export async function resolveURLWithHistory(
removeDynamicAgent: async () => {},
forceCleanupDynamicAgent: async () => {},
getSharedLocalHostPort: async () => 0,
setLocalHostPort: (_port: number) => {},
indexes: async () => [],
reloadAgentSchema: async () => {},
};
Expand Down
34 changes: 31 additions & 3 deletions ts/packages/agents/browser/src/views/server/core/baseServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export class BaseServer {
private sseManager: SSEManager;
private features: Map<string, FeatureConfig> = new Map();
private config: ServerConfig;
private boundPort: number | undefined;

constructor(config: ServerConfig) {
this.config = config;
Expand Down Expand Up @@ -142,18 +143,45 @@ export class BaseServer {
return this.app;
}

get port(): number {
if (this.boundPort === undefined) {
throw new Error("Server has not been started yet.");
}
return this.boundPort;
}

/**
* Start the server
*/
start(): Promise<void> {
return new Promise((resolve) => {
this.app.listen(this.config.port, () => {
debug(`Server running at http://localhost:${this.config.port}`);
return new Promise((resolve, reject) => {
const server = this.app.listen(this.config.port, () => {
this.boundPort = (server.address() as { port: number }).port;
debug(`Server running at http://localhost:${this.boundPort}`);
debug(
`Registered features: ${Array.from(this.features.keys()).join(", ")}`,
);
server.removeListener("error", onStartupError);
server.on("error", (err: NodeJS.ErrnoException) => {
console.error(
`Server runtime error on port ${this.boundPort}:`,
err,
);
});
resolve();
});
const onStartupError = (err: NodeJS.ErrnoException) => {
if (err.code === "EADDRINUSE") {
reject(
new Error(
`Port ${this.config.port} is already in use. Is another instance already running?`,
),
);
} else {
reject(err);
}
};
server.on("error", onStartupError);
});
}
}
2 changes: 1 addition & 1 deletion ts/packages/agents/browser/src/views/server/server.mts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ async function main() {
await server.start();

// Process lifecycle management
process.send?.("Success");
process.send?.({ type: "Success", port: server.port });

process.on("message", (message: any) => {
debug("Received message:", message);
Expand Down
90 changes: 49 additions & 41 deletions ts/packages/agents/markdown/src/agent/markdownActionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,10 +297,15 @@ async function updateMarkdownContext(
const fullPath = await getFullMarkdownFilePath(fileName, storage!);
if (fullPath) {
process.env.MARKDOWN_FILE = fullPath;
context.agentContext.viewProcess = await createViewServiceHost(
const result = await createViewServiceHost(
fullPath,
context.agentContext.localHostPort,
);
if (result) {
context.agentContext.viewProcess = result.process;
context.agentContext.localHostPort = result.port;
context.setLocalHostPort(result.port);
}
}
}

Expand Down Expand Up @@ -878,7 +883,10 @@ async function getDocumentContentFromView(
// NOTE: Function commented out per Flow 1 consolidation
// Collaboration server now managed by view process

async function createViewServiceHost(filePath: string, port: number) {
async function createViewServiceHost(
filePath: string,
port: number,
): Promise<{ process: ChildProcess; port: number } | undefined> {
let timeoutHandle: NodeJS.Timeout;

const timeoutPromise = new Promise<undefined>((_resolve, reject) => {
Expand All @@ -888,47 +896,47 @@ async function createViewServiceHost(filePath: string, port: number) {
}, 10000);
});

const viewServicePromise = new Promise<ChildProcess | undefined>(
(resolve, reject) => {
try {
const expressService = fileURLToPath(
new URL(
path.join("..", "./view/route/service.js"),
import.meta.url,
),
);
const viewServicePromise = new Promise<
{ process: ChildProcess; port: number } | undefined
>((resolve, reject) => {
try {
const expressService = fileURLToPath(
new URL(
path.join("..", "./view/route/service.js"),
import.meta.url,
),
);

const folderPath = path.dirname(filePath!);
const folderPath = path.dirname(filePath!);

const childProcess = fork(expressService, [port.toString()], {
env: {
...process.env,
TYPEAGENT_MARKDOWN_ROOT: folderPath,
},
});

childProcess.send({
type: "setFile",
filePath: path.basename(filePath),
});

childProcess.on("message", function (message: any) {
if (message === "Success") {
resolve(childProcess);
} else if (message === "Failure") {
resolve(undefined);
}
});

childProcess.on("exit", (code) => {
debug("Markdown view server exited with code:", code);
});
} catch (e: any) {
console.error(e);
resolve(undefined);
}
},
);
const childProcess = fork(expressService, [port.toString()], {
env: {
...process.env,
TYPEAGENT_MARKDOWN_ROOT: folderPath,
},
});

childProcess.send({
type: "setFile",
filePath: path.basename(filePath),
});

childProcess.on("message", function (message: any) {
if (message?.type === "Success") {
resolve({ process: childProcess, port: message.port });
} else if (message === "Failure") {
resolve(undefined);
}
});

childProcess.on("exit", (code) => {
debug("Markdown view server exited with code:", code);
});
} catch (e: any) {
console.error(e);
resolve(undefined);
}
});

return Promise.race([viewServicePromise, timeoutPromise]).then((result) => {
clearTimeout(timeoutHandle);
Expand Down
17 changes: 14 additions & 3 deletions ts/packages/agents/markdown/src/view/route/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2245,9 +2245,20 @@ debug(`[SIGNAL] Y.js WebSocket server integrated`);

// Start the HTTP server (which includes WebSocket support)
server.listen(port, () => {
debug(`Express server with WebSocket support listening on port ${port}`);
debug(`Y.js collaboration available at ws://localhost:${port}/<room-name>`);
const boundPort = (server.address() as { port: number }).port;
debug(
`Express server with WebSocket support listening on port ${boundPort}`,
);
debug(
`Y.js collaboration available at ws://localhost:${boundPort}/<room-name>`,
);

// Send success signal to parent process AFTER server is ready to accept WebSocket connections
process.send?.("Success");
process.send?.({ type: "Success", port: boundPort });
});

server.on("error", (err: NodeJS.ErrnoException) => {
console.error("Markdown view server failed to start:", err);
process.send?.("Failure");
process.exit(1);
});
Loading
Loading