Skip to content

Commit 31a1ea6

Browse files
session management and http server code
1 parent e710c39 commit 31a1ea6

File tree

15 files changed

+414
-1503
lines changed

15 files changed

+414
-1503
lines changed

bun.lock

Lines changed: 79 additions & 2 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
"smol-toml": "^1.4.2"
5959
},
6060
"devDependencies": {
61+
"@ai-sdk/provider": "2.0.0",
62+
"@ai-sdk/ui-utils": "^1.2.11",
6163
"@eslint/js": "^9.35.0",
6264
"@modelcontextprotocol/sdk": "1.20.0",
6365
"@stylistic/eslint-plugin": "^5.4.0",
@@ -69,6 +71,7 @@
6971
"@types/sinon": "^17.0.4",
7072
"@typescript-eslint/eslint-plugin": "^8.43.0",
7173
"@typescript-eslint/parser": "^8.43.0",
74+
"ai": "^5.0.102",
7275
"async-mutex": "^0.5.0",
7376
"chrome-devtools-frontend": "1.0.1524741",
7477
"commander": "^14.0.1",

packages/agent/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,22 @@
3434
"@ai-sdk/google": "^2.0.43",
3535
"@ai-sdk/openai": "^2.0.72",
3636
"@ai-sdk/openai-compatible": "^1.0.27",
37+
"@ai-sdk/provider": "2.0.0",
38+
"@ai-sdk/ui-utils": "^1.2.11",
3739
"@anthropic-ai/claude-agent-sdk": "^0.1.11",
3840
"@browseros/common": "workspace:*",
3941
"@browseros/server": "workspace:*",
4042
"@browseros/tools": "workspace:*",
4143
"@google/gemini-cli-core": "^0.16.0",
44+
"@hono/node-server": "^1.19.6",
4245
"@openrouter/ai-sdk-provider": "~1.2.5",
4346
"ai": "^5.0.101",
4447
"zod": "^4.1.12"
4548
},
4649
"devDependencies": {
4750
"@types/bun": "latest",
48-
"typescript": "^5.9.3"
51+
"typescript": "^5.9.3",
52+
"vitest": "^4.0.14"
4953
},
5054
"optionalDependencies": {
5155
"chrome-devtools-mcp": "latest"

packages/agent/src/errors.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
export class HttpAgentError extends Error {
2+
constructor(
3+
message: string,
4+
public statusCode: number = 500,
5+
public code?: string,
6+
) {
7+
super(message);
8+
this.name = this.constructor.name;
9+
Error.captureStackTrace(this, this.constructor);
10+
}
11+
12+
toJSON() {
13+
return {
14+
error: {
15+
name: this.name,
16+
message: this.message,
17+
code: this.code,
18+
statusCode: this.statusCode,
19+
},
20+
};
21+
}
22+
}
23+
24+
export class ValidationError extends HttpAgentError {
25+
constructor(message: string, public details?: unknown) {
26+
super(message, 400, 'VALIDATION_ERROR');
27+
}
28+
29+
override toJSON() {
30+
return {
31+
error: {
32+
name: this.name,
33+
message: this.message,
34+
code: this.code,
35+
statusCode: this.statusCode,
36+
details: this.details,
37+
},
38+
};
39+
}
40+
}
41+
42+
export class SessionNotFoundError extends HttpAgentError {
43+
constructor(public conversationId: string) {
44+
super(`Session "${conversationId}" not found.`, 404, 'SESSION_NOT_FOUND');
45+
}
46+
}
47+
48+
export class AgentExecutionError extends HttpAgentError {
49+
constructor(message: string, public originalError?: Error) {
50+
super(message, 500, 'AGENT_EXECUTION_ERROR');
51+
}
52+
53+
override toJSON() {
54+
return {
55+
error: {
56+
name: this.name,
57+
message: this.message,
58+
code: this.code,
59+
statusCode: this.statusCode,
60+
originalError: this.originalError?.message,
61+
},
62+
};
63+
}
64+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { Hono } from 'hono';
2+
import { cors } from 'hono/cors';
3+
import { stream } from 'hono/streaming';
4+
import { serve } from '@hono/node-server';
5+
import { formatDataStreamPart } from '@ai-sdk/ui-utils';
6+
import { logger } from '@browseros/common';
7+
import type { Context, Next } from 'hono';
8+
import type { ContentfulStatusCode } from 'hono/utils/http-status';
9+
import type { z } from 'zod';
10+
11+
import { SessionManager } from '../session/SessionManager.js';
12+
import { HttpAgentError, ValidationError, AgentExecutionError } from '../errors.js';
13+
import { ChatRequestSchema, HttpServerConfigSchema } from './types.js';
14+
import type { HttpServerConfig, ValidatedHttpServerConfig, ChatRequest } from './types.js';
15+
16+
type AppVariables = {
17+
validatedBody: unknown;
18+
};
19+
20+
const DEFAULT_MCP_SERVER_URL = 'http://127.0.0.1:9150/mcp';
21+
const DEFAULT_TEMP_DIR = '/tmp';
22+
23+
function validateRequest<T>(schema: z.ZodType<T>) {
24+
return async (c: Context<{ Variables: AppVariables }>, next: Next) => {
25+
try {
26+
const body = await c.req.json();
27+
const validated = schema.parse(body);
28+
c.set('validatedBody', validated);
29+
await next();
30+
} catch (err) {
31+
if (err && typeof err === 'object' && 'issues' in err) {
32+
const zodError = err as { issues: unknown };
33+
logger.warn('Request validation failed', { issues: zodError.issues });
34+
throw new ValidationError('Request validation failed', zodError.issues);
35+
}
36+
throw err;
37+
}
38+
};
39+
}
40+
41+
export function createHttpServer(config: HttpServerConfig) {
42+
const validatedConfig: ValidatedHttpServerConfig = HttpServerConfigSchema.parse(config);
43+
const mcpServerUrl = validatedConfig.mcpServerUrl || process.env.MCP_SERVER_URL || DEFAULT_MCP_SERVER_URL;
44+
45+
const app = new Hono<{ Variables: AppVariables }>();
46+
const sessionManager = new SessionManager();
47+
48+
app.use(
49+
'/*',
50+
cors({
51+
origin: validatedConfig.corsOrigins,
52+
allowMethods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
53+
allowHeaders: ['Content-Type', 'Authorization'],
54+
}),
55+
);
56+
57+
app.onError((err, c) => {
58+
const error = err as Error;
59+
60+
if (error instanceof HttpAgentError) {
61+
logger.warn('HTTP Agent Error', {
62+
name: error.name,
63+
message: error.message,
64+
code: error.code,
65+
statusCode: error.statusCode,
66+
});
67+
return c.json(error.toJSON(), error.statusCode as ContentfulStatusCode);
68+
}
69+
70+
logger.error('Unhandled Error', {
71+
message: error.message,
72+
stack: error.stack,
73+
});
74+
75+
return c.json(
76+
{
77+
error: {
78+
name: 'InternalServerError',
79+
message: error.message || 'An unexpected error occurred',
80+
code: 'INTERNAL_SERVER_ERROR',
81+
statusCode: 500,
82+
},
83+
},
84+
500,
85+
);
86+
});
87+
88+
app.get('/health', (c) => c.json({ status: 'ok' }));
89+
90+
app.post('/chat', validateRequest(ChatRequestSchema), async (c) => {
91+
const request = c.get('validatedBody') as ChatRequest;
92+
93+
logger.info('Chat request received', {
94+
conversationId: request.conversationId,
95+
provider: request.provider,
96+
model: request.model,
97+
});
98+
99+
c.header('Content-Type', 'text/plain; charset=utf-8');
100+
c.header('X-Vercel-AI-Data-Stream', 'v1');
101+
c.header('Cache-Control', 'no-cache');
102+
c.header('Connection', 'keep-alive');
103+
104+
return stream(c, async (honoStream) => {
105+
try {
106+
const agent = await sessionManager.getOrCreate({
107+
conversationId: request.conversationId,
108+
provider: request.provider,
109+
model: request.model,
110+
apiKey: request.apiKey,
111+
baseUrl: request.baseUrl,
112+
// Azure-specific
113+
resourceName: request.resourceName,
114+
// AWS Bedrock-specific
115+
region: request.region,
116+
accessKeyId: request.accessKeyId,
117+
secretAccessKey: request.secretAccessKey,
118+
sessionToken: request.sessionToken,
119+
// Agent-specific
120+
tempDir: validatedConfig.tempDir || DEFAULT_TEMP_DIR,
121+
mcpServerUrl,
122+
});
123+
124+
await agent.execute(request.message, honoStream);
125+
} catch (error) {
126+
const errorMessage = error instanceof Error ? error.message : 'Agent execution failed';
127+
logger.error('Agent execution error', {
128+
conversationId: request.conversationId,
129+
error: errorMessage,
130+
});
131+
await honoStream.write(formatDataStreamPart('error', errorMessage));
132+
throw new AgentExecutionError('Agent execution failed', error instanceof Error ? error : undefined);
133+
}
134+
});
135+
});
136+
137+
app.delete('/chat/:conversationId', (c) => {
138+
const conversationId = c.req.param('conversationId');
139+
const deleted = sessionManager.delete(conversationId);
140+
141+
if (deleted) {
142+
return c.json({
143+
success: true,
144+
message: `Session ${conversationId} deleted`,
145+
sessionCount: sessionManager.count(),
146+
});
147+
}
148+
149+
return c.json({
150+
success: false,
151+
message: `Session ${conversationId} not found`,
152+
}, 404);
153+
});
154+
155+
const server = serve({
156+
fetch: app.fetch,
157+
port: validatedConfig.port,
158+
hostname: validatedConfig.host,
159+
});
160+
161+
logger.info('HTTP Agent Server started', {
162+
port: validatedConfig.port,
163+
host: validatedConfig.host,
164+
});
165+
166+
return {
167+
app,
168+
server,
169+
config: validatedConfig,
170+
};
171+
}

packages/agent/src/http/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { createHttpServer } from './HttpServer.js';
2+
export { HttpServerConfigSchema, ChatRequestSchema } from './types.js';
3+
export type { HttpServerConfig, ValidatedHttpServerConfig, ChatRequest } from './types.js';

packages/agent/src/http/types.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { z } from 'zod';
2+
import { VercelAIConfigSchema } from '../agent/gemini-vercel-sdk-adapter/types.js';
3+
4+
/**
5+
* Chat request schema extends VercelAIConfig with request-specific fields
6+
*/
7+
export const ChatRequestSchema = VercelAIConfigSchema.extend({
8+
conversationId: z.string().uuid(),
9+
message: z.string().min(1, 'Message cannot be empty'),
10+
});
11+
12+
export type ChatRequest = z.infer<typeof ChatRequestSchema>;
13+
14+
export interface HttpServerConfig {
15+
port: number;
16+
host?: string;
17+
corsOrigins?: string[];
18+
tempDir?: string;
19+
mcpServerUrl?: string;
20+
}
21+
22+
export const HttpServerConfigSchema = z.object({
23+
port: z.number().int().positive(),
24+
host: z.string().optional().default('0.0.0.0'),
25+
corsOrigins: z.array(z.string()).optional().default(['*']),
26+
tempDir: z.string().optional().default('/tmp'),
27+
mcpServerUrl: z.string().optional(),
28+
});
29+
30+
export type ValidatedHttpServerConfig = z.infer<typeof HttpServerConfigSchema>;

packages/agent/src/index.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1-
/**
2-
* @license
3-
* Copyright 2025 BrowserOS
4-
*/
1+
export { createHttpServer } from './http/index.js';
2+
export { HttpServerConfigSchema, ChatRequestSchema } from './http/index.js';
3+
export type { HttpServerConfig, ValidatedHttpServerConfig, ChatRequest } from './http/index.js';
54

6-
// Public API exports for integration with main server
7-
export {createServer as createAgentServer} from './websocket/server.js';
8-
export {ServerConfigSchema as AgentServerConfigSchema} from './websocket/server.js';
9-
export type {ServerConfig as AgentServerConfig} from './websocket/server.js';
10-
export type {ControllerBridge} from '@browseros/controller-server';
5+
// Alias for backwards compatibility with packages/server
6+
export { createHttpServer as createAgentServer } from './http/index.js';
7+
export type { HttpServerConfig as AgentServerConfig } from './http/index.js';
118

12-
// Agent factory exports
13-
export {AgentFactory} from './agent/AgentFactory.js';
14-
export type {AgentConstructor} from './agent/AgentFactory.js';
15-
export {registerAgents} from './agent/registry.js';
16-
export {BaseAgent} from './agent/BaseAgent.js';
9+
export { GeminiAgent, AIProvider } from './agent/index.js';
10+
export type { AgentConfig } from './agent/index.js';
11+
12+
export { SessionManager } from './session/index.js';
13+
14+
export { HttpAgentError, ValidationError, SessionNotFoundError, AgentExecutionError } from './errors.js';

0 commit comments

Comments
 (0)