Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/coc-client/src/contracts/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export interface RuntimeDashboardConfig {
excalidrawEnabled: boolean;
mcpOauthEnabled: boolean;
focusedDiffEnabled: boolean;
containerDefaultAgentEnabled: boolean;
codexEnabled: boolean;
activeProvider: 'copilot' | 'codex';
};
Expand Down
11 changes: 11 additions & 0 deletions packages/coc/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ export interface CLIConfig {
excalidraw?: {
enabled?: boolean;
};
/** Container default agent — smart routing session. Disabled by default. */
containerDefaultAgent?: {
enabled?: boolean;
};
/** Codex SDK provider support. Disabled by default. */
codex?: {
enabled?: boolean;
Expand Down Expand Up @@ -318,6 +322,10 @@ export interface ResolvedCLIConfig {
excalidraw: {
enabled: boolean;
};
/** Container default agent — smart routing session. */
containerDefaultAgent: {
enabled: boolean;
};
/** Codex SDK provider support. Disabled by default. */
codex: {
enabled: boolean;
Expand Down Expand Up @@ -457,6 +465,9 @@ export const DEFAULT_CONFIG: ResolvedCLIConfig = {
excalidraw: {
enabled: false,
},
containerDefaultAgent: {
enabled: false,
},
codex: {
enabled: false,
},
Expand Down
8 changes: 8 additions & 0 deletions packages/coc/src/config/namespace-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type ResolvedConfigNamespaceValues = Pick<
| 'loops'
| 'mcpOauth'
| 'excalidraw'
| 'containerDefaultAgent'
| 'codex'
| 'features'
| 'memoryPromotion'
Expand Down Expand Up @@ -72,6 +73,7 @@ const VIM_NAVIGATION_SOURCE_KEYS = ['vimNavigation.enabled'] as const;
const LOOPS_SOURCE_KEYS = ['loops.enabled'] as const;
const MCP_OAUTH_SOURCE_KEYS = ['mcpOauth.enabled'] as const;
const EXCALIDRAW_SOURCE_KEYS = ['excalidraw.enabled'] as const;
const CONTAINER_DEFAULT_AGENT_SOURCE_KEYS = ['containerDefaultAgent.enabled'] as const;
const CODEX_SOURCE_KEYS = ['codex.enabled'] as const;
const FEATURES_SOURCE_KEYS = ['features.autoMemoryPromotion', 'features.focusedDiff'] as const;

Expand Down Expand Up @@ -104,6 +106,7 @@ export const CONFIG_NAMESPACE_SOURCE_KEYS = [
...LOOPS_SOURCE_KEYS,
...MCP_OAUTH_SOURCE_KEYS,
...EXCALIDRAW_SOURCE_KEYS,
...CONTAINER_DEFAULT_AGENT_SOURCE_KEYS,
...CODEX_SOURCE_KEYS,
...FEATURES_SOURCE_KEYS,
...MEMORY_PROMOTION_SOURCE_KEYS,
Expand Down Expand Up @@ -257,6 +260,11 @@ export function createConfigNamespaceRegistry(defaultBundledSkills: readonly str
sourceDescriptors: [source('excalidraw.', ['excalidraw'], EXCALIDRAW_SOURCE_KEYS)],
merge: (base, override) => ({ excalidraw: { enabled: override?.excalidraw?.enabled ?? base.excalidraw?.enabled ?? false } }),
},
{
name: 'containerDefaultAgent',
sourceDescriptors: [source('containerDefaultAgent.', ['containerDefaultAgent'], CONTAINER_DEFAULT_AGENT_SOURCE_KEYS)],
merge: (base, override) => ({ containerDefaultAgent: { enabled: override?.containerDefaultAgent?.enabled ?? base.containerDefaultAgent?.enabled ?? false } }),
},
{
name: 'codex',
sourceDescriptors: [source('codex.', ['codex'], CODEX_SOURCE_KEYS)],
Expand Down
4 changes: 4 additions & 0 deletions packages/coc/src/server/admin/admin-config-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ export const ADMIN_CONFIG_FIELDS: readonly AdminConfigFieldSpec[] = [
if (!cfg.mcpOauth) { cfg.mcpOauth = {}; }
cfg.mcpOauth.enabled = v;
}, 'restartRequired'),
bool('containerDefaultAgent.enabled', (cfg, v) => {
if (!cfg.containerDefaultAgent) { cfg.containerDefaultAgent = {}; }
cfg.containerDefaultAgent.enabled = v;
}),
bool('codex.enabled', (cfg, v) => {
if (!cfg.codex) { cfg.codex = {}; }
cfg.codex.enabled = v;
Expand Down
1 change: 1 addition & 0 deletions packages/coc/src/server/config/runtime-config-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function buildRuntimeDashboardConfig(
excalidrawEnabled: config.excalidraw?.enabled ?? false,
mcpOauthEnabled: config.mcpOauth?.enabled ?? false,
focusedDiffEnabled: config.features?.focusedDiff ?? false,
containerDefaultAgentEnabled: config.containerDefaultAgent?.enabled ?? false,
codexEnabled: config.codex?.enabled ?? false,
activeProvider: config.activeProvider ?? 'copilot',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/**
* Container Session Handler
*
* REST API routes for container sessions:
* POST /api/container/sessions — Create a new container session
* GET /api/container/sessions — List container sessions
* GET /api/container/sessions/:id — Get session detail with turns
* POST /api/container/sessions/:id/message — Send a message (triggers routing)
* PATCH /api/container/sessions/:id/routing — Set/clear routing override
* DELETE /api/container/sessions/:id — Delete a container session
*/

import { randomBytes } from 'crypto';
import type { Route } from '../types';
import { sendJson, readBody, sendError } from '../shared/router';
import type { ContainerSessionStore } from './container-session-store';
import type { RoutingClassifierDeps } from './routing-classifier';
import type { ContainerAgentInfo, RoutingDecision } from './container-session-types';
import { classifyRouting } from './routing-classifier';

// ============================================================================
// Types
// ============================================================================

export interface ContainerSessionRouteOptions {
store: ContainerSessionStore;
classifierDeps: RoutingClassifierDeps;
/** Returns available agents and their workspaces for routing. */
getAgents: () => Promise<ContainerAgentInfo[]>;
/** Forwards a message to a specific agent's queue. Returns downstream process ID. */
forwardMessage: (agentId: string, workspaceId: string, message: string, existingProcessId?: string | null) => Promise<string>;
}

// ============================================================================
// Route Registration
// ============================================================================

export function registerContainerSessionRoutes(
routes: Route[],
options: ContainerSessionRouteOptions,
): void {
const { store, classifierDeps, getAgents, forwardMessage } = options;

// POST /api/container/sessions — create session
routes.push({
method: 'POST',
pattern: '/api/container/sessions',
handler: (_req, res) => {
const id = `csess_${randomBytes(8).toString('hex')}`;
const session = store.create(id);
sendJson(res, session, 201);
},
});

// GET /api/container/sessions — list sessions
routes.push({
method: 'GET',
pattern: '/api/container/sessions',
handler: (req, res) => {
const url = new URL(req.url ?? '', 'http://localhost');
const limit = parseInt(url.searchParams.get('limit') ?? '50', 10);
const offset = parseInt(url.searchParams.get('offset') ?? '0', 10);
const sessions = store.list(limit, offset);
sendJson(res, sessions);
},
});

// GET /api/container/sessions/:id — get session detail
routes.push({
method: 'GET',
pattern: /^\/api\/container\/sessions\/([^/]+)$/,
handler: (_req, res, match) => {
const id = decodeURIComponent(match![1]);
const session = store.get(id);
if (!session) return sendError(res, 404, 'Session not found');
sendJson(res, session);
},
});

// POST /api/container/sessions/:id/message — send message
routes.push({
method: 'POST',
pattern: /^\/api\/container\/sessions\/([^/]+)\/message$/,
handler: async (req, res, match) => {
try {
const id = decodeURIComponent(match![1]);
const session = store.get(id);
if (!session) return sendError(res, 404, 'Session not found');
if (session.status === 'closed') return sendError(res, 400, 'Session is closed');

const body = await readBody(req);
const { content } = JSON.parse(body);
if (!content || typeof content !== 'string') {
return sendError(res, 400, 'content is required');
}

// Classify routing
const agents = await getAgents();
const routing: RoutingDecision = await classifyRouting(
{
agents,
history: session.turns,
message: content,
override: session.routingOverride,
},
classifierDeps,
);

// Find existing downstream process for this agent:workspace
const existingProcessId = findExistingDownstreamProcess(session.turns, routing);

// Forward message to target agent
const downstreamProcessId = await forwardMessage(
routing.agentId,
routing.workspaceId,
content,
existingProcessId,
);

// Record user turn
const turnIndex = session.turns.length;
const userTurn = {
index: turnIndex,
role: 'user' as const,
content,
routing,
downstreamProcessId,
timestamp: new Date().toISOString(),
};
store.addTurn(id, userTurn);

sendJson(res, {
turn: userTurn,
routing,
downstreamProcessId,
});
} catch (err: any) {
sendError(res, 500, err.message ?? 'Internal error');
}
},
});

// PATCH /api/container/sessions/:id/routing — set/clear override
routes.push({
method: 'PATCH',
pattern: /^\/api\/container\/sessions\/([^/]+)\/routing$/,
handler: async (req, res, match) => {
try {
const id = decodeURIComponent(match![1]);
const session = store.get(id);
if (!session) return sendError(res, 404, 'Session not found');

const body = await readBody(req);
const { agentId, workspaceId } = JSON.parse(body);

const override = agentId && workspaceId
? { agentId, workspaceId }
: null;

store.setRoutingOverride(id, override);
sendJson(res, { routingOverride: override });
} catch (err: any) {
sendError(res, 500, err.message ?? 'Internal error');
}
},
});

// DELETE /api/container/sessions/:id — delete session
routes.push({
method: 'DELETE',
pattern: /^\/api\/container\/sessions\/([^/]+)$/,
handler: (_req, res, match) => {
const id = decodeURIComponent(match![1]);
const deleted = store.delete(id);
if (!deleted) return sendError(res, 404, 'Session not found');
res.writeHead(204);
res.end();
},
});
}

// ============================================================================
// Helpers
// ============================================================================

function findExistingDownstreamProcess(
turns: Array<{ routing: RoutingDecision; downstreamProcessId: string | null }>,
routing: RoutingDecision,
): string | null {
for (let i = turns.length - 1; i >= 0; i--) {
const t = turns[i];
if (t.routing.agentId === routing.agentId && t.routing.workspaceId === routing.workspaceId && t.downstreamProcessId) {
return t.downstreamProcessId;
}
}
return null;
}
Loading
Loading