mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 11:02:17 +00:00
commit
fcdfe82bd8
3 changed files with 189 additions and 60 deletions
|
|
@ -56,6 +56,14 @@ async function readPipedStdin(): Promise<string | undefined> {
|
|||
});
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const GATEWAY_RESTART_DELAY_MS = 2000;
|
||||
const GATEWAY_MIN_RUNTIME_MS = 10000;
|
||||
const GATEWAY_MAX_CONSECUTIVE_FAILURES = 10;
|
||||
|
||||
function reportSettingsErrors(settingsManager: SettingsManager, context: string): void {
|
||||
const errors = settingsManager.drainErrors();
|
||||
for (const { scope, error } of errors) {
|
||||
|
|
@ -744,6 +752,150 @@ export async function main(args: string[]) {
|
|||
authStorage.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey);
|
||||
}
|
||||
|
||||
const cliThinkingOverride = parsed.thinking !== undefined || cliThinkingFromModel;
|
||||
|
||||
if (isGatewayCommand) {
|
||||
const gatewayLoaderOptions = {
|
||||
additionalExtensionPaths: firstPass.extensions,
|
||||
additionalSkillPaths: firstPass.skills,
|
||||
additionalPromptTemplatePaths: firstPass.promptTemplates,
|
||||
additionalThemePaths: firstPass.themes,
|
||||
noExtensions: firstPass.noExtensions,
|
||||
noSkills: firstPass.noSkills,
|
||||
noPromptTemplates: firstPass.noPromptTemplates,
|
||||
noThemes: firstPass.noThemes,
|
||||
systemPrompt: firstPass.systemPrompt,
|
||||
appendSystemPrompt: firstPass.appendSystemPrompt,
|
||||
};
|
||||
const gatewaySessionRoot = join(agentDir, "gateway-sessions");
|
||||
let consecutiveFailures = 0;
|
||||
let primarySessionFile = sessionManager?.getSessionFile();
|
||||
const persistPrimarySession = sessionManager ? sessionManager.isPersisted() : !parsed.noSession;
|
||||
|
||||
const createPrimarySessionManager = (): SessionManager => {
|
||||
if (!persistPrimarySession) {
|
||||
return SessionManager.inMemory(cwd);
|
||||
}
|
||||
if (primarySessionFile) {
|
||||
return SessionManager.open(primarySessionFile, parsed.sessionDir);
|
||||
}
|
||||
return SessionManager.create(cwd, parsed.sessionDir);
|
||||
};
|
||||
|
||||
const createGatewaySession = async (sessionManagerForRun: SessionManager) => {
|
||||
const gatewayResourceLoader = new DefaultResourceLoader({
|
||||
cwd,
|
||||
agentDir,
|
||||
settingsManager,
|
||||
...gatewayLoaderOptions,
|
||||
});
|
||||
await gatewayResourceLoader.reload();
|
||||
|
||||
const result = await createAgentSession({
|
||||
...sessionOptions,
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
settingsManager,
|
||||
resourceLoader: gatewayResourceLoader,
|
||||
sessionManager: sessionManagerForRun,
|
||||
});
|
||||
|
||||
primarySessionFile = result.session.sessionManager.getSessionFile();
|
||||
return result;
|
||||
};
|
||||
|
||||
while (true) {
|
||||
const primarySessionManager = createPrimarySessionManager();
|
||||
const { session, modelFallbackMessage } = await createGatewaySession(primarySessionManager);
|
||||
|
||||
if (!session.model) {
|
||||
console.error(chalk.red("No models available."));
|
||||
console.error(chalk.yellow("\nSet an API key environment variable:"));
|
||||
console.error(" ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, etc.");
|
||||
console.error(chalk.yellow(`\nOr create ${getModelsPath()}`));
|
||||
if (modelFallbackMessage) {
|
||||
console.error(chalk.dim(modelFallbackMessage));
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (cliThinkingOverride) {
|
||||
let effectiveThinking = session.thinkingLevel;
|
||||
if (!session.model.reasoning) {
|
||||
effectiveThinking = "off";
|
||||
} else if (effectiveThinking === "xhigh" && !supportsXhigh(session.model)) {
|
||||
effectiveThinking = "high";
|
||||
}
|
||||
if (effectiveThinking !== session.thinkingLevel) {
|
||||
session.setThinkingLevel(effectiveThinking);
|
||||
}
|
||||
}
|
||||
|
||||
const daemonOptions: DaemonModeOptions = {
|
||||
initialMessage,
|
||||
initialImages,
|
||||
messages: parsed.messages,
|
||||
gateway: settingsManager.getGatewaySettings(),
|
||||
createSession: async (sessionKey) => {
|
||||
const gatewayResourceLoader = new DefaultResourceLoader({
|
||||
cwd,
|
||||
agentDir,
|
||||
settingsManager,
|
||||
...gatewayLoaderOptions,
|
||||
});
|
||||
await gatewayResourceLoader.reload();
|
||||
const gatewaySessionOptions: CreateAgentSessionOptions = {
|
||||
...sessionOptions,
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
settingsManager,
|
||||
resourceLoader: gatewayResourceLoader,
|
||||
sessionManager: createGatewaySessionManager(cwd, sessionKey, gatewaySessionRoot),
|
||||
};
|
||||
const { session: gatewaySession } = await createAgentSession(gatewaySessionOptions);
|
||||
return gatewaySession;
|
||||
},
|
||||
};
|
||||
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
const result = await runDaemonMode(session, daemonOptions);
|
||||
if (result.reason === "shutdown") {
|
||||
stopThemeWatcher();
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.stack || error.message : String(error);
|
||||
console.error(`[pi-gateway] daemon crashed: ${message}`);
|
||||
try {
|
||||
session.dispose();
|
||||
} catch {
|
||||
// Ignore disposal errors during crash handling.
|
||||
}
|
||||
}
|
||||
|
||||
const runtimeMs = Date.now() - startedAt;
|
||||
if (runtimeMs < GATEWAY_MIN_RUNTIME_MS) {
|
||||
consecutiveFailures += 1;
|
||||
console.error(
|
||||
`[pi-gateway] exited quickly (${runtimeMs}ms), failure ${consecutiveFailures}/${GATEWAY_MAX_CONSECUTIVE_FAILURES}`,
|
||||
);
|
||||
if (consecutiveFailures >= GATEWAY_MAX_CONSECUTIVE_FAILURES) {
|
||||
console.error("[pi-gateway] crash loop detected, exiting");
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
consecutiveFailures = 0;
|
||||
console.error(`[pi-gateway] exited after ${runtimeMs}ms, restarting`);
|
||||
}
|
||||
|
||||
if (GATEWAY_RESTART_DELAY_MS > 0) {
|
||||
console.error(`[pi-gateway] restarting in ${GATEWAY_RESTART_DELAY_MS}ms`);
|
||||
await sleep(GATEWAY_RESTART_DELAY_MS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { session, modelFallbackMessage } = await createAgentSession(sessionOptions);
|
||||
|
||||
if (!isInteractive && !session.model) {
|
||||
|
|
@ -756,7 +908,6 @@ export async function main(args: string[]) {
|
|||
|
||||
// Clamp thinking level to model capabilities for CLI-provided thinking levels.
|
||||
// This covers both --thinking <level> and --model <pattern>:<thinking>.
|
||||
const cliThinkingOverride = parsed.thinking !== undefined || cliThinkingFromModel;
|
||||
if (session.model && cliThinkingOverride) {
|
||||
let effectiveThinking = session.thinkingLevel;
|
||||
if (!session.model.reasoning) {
|
||||
|
|
@ -792,46 +943,6 @@ export async function main(args: string[]) {
|
|||
verbose: parsed.verbose,
|
||||
});
|
||||
await mode.run();
|
||||
} else if (isGatewayCommand) {
|
||||
const gatewayLoaderOptions = {
|
||||
additionalExtensionPaths: firstPass.extensions,
|
||||
additionalSkillPaths: firstPass.skills,
|
||||
additionalPromptTemplatePaths: firstPass.promptTemplates,
|
||||
additionalThemePaths: firstPass.themes,
|
||||
noExtensions: firstPass.noExtensions,
|
||||
noSkills: firstPass.noSkills,
|
||||
noPromptTemplates: firstPass.noPromptTemplates,
|
||||
noThemes: firstPass.noThemes,
|
||||
systemPrompt: firstPass.systemPrompt,
|
||||
appendSystemPrompt: firstPass.appendSystemPrompt,
|
||||
};
|
||||
const gatewaySessionRoot = join(agentDir, "gateway-sessions");
|
||||
const daemonOptions: DaemonModeOptions = {
|
||||
initialMessage,
|
||||
initialImages,
|
||||
messages: parsed.messages,
|
||||
gateway: settingsManager.getGatewaySettings(),
|
||||
createSession: async (sessionKey) => {
|
||||
const gatewayResourceLoader = new DefaultResourceLoader({
|
||||
cwd,
|
||||
agentDir,
|
||||
settingsManager,
|
||||
...gatewayLoaderOptions,
|
||||
});
|
||||
await gatewayResourceLoader.reload();
|
||||
const gatewaySessionOptions: CreateAgentSessionOptions = {
|
||||
...sessionOptions,
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
settingsManager,
|
||||
resourceLoader: gatewayResourceLoader,
|
||||
sessionManager: createGatewaySessionManager(cwd, sessionKey, gatewaySessionRoot),
|
||||
};
|
||||
const { session: gatewaySession } = await createAgentSession(gatewaySessionOptions);
|
||||
return gatewaySession;
|
||||
},
|
||||
};
|
||||
await runDaemonMode(session, daemonOptions);
|
||||
} else {
|
||||
await runPrintMode(session, {
|
||||
mode,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ export interface DaemonModeOptions {
|
|||
gateway: GatewaySettings;
|
||||
}
|
||||
|
||||
export interface DaemonModeResult {
|
||||
reason: "shutdown";
|
||||
}
|
||||
|
||||
function createCommandContextActions(session: AgentSession) {
|
||||
return {
|
||||
waitForIdle: () => session.agent.waitForIdle(),
|
||||
|
|
@ -70,11 +74,11 @@ function createCommandContextActions(session: AgentSession) {
|
|||
* Run in daemon mode.
|
||||
* Stays alive indefinitely unless stopped by signal or extension trigger.
|
||||
*/
|
||||
export async function runDaemonMode(session: AgentSession, options: DaemonModeOptions): Promise<never> {
|
||||
export async function runDaemonMode(session: AgentSession, options: DaemonModeOptions): Promise<DaemonModeResult> {
|
||||
const { initialMessage, initialImages, messages = [] } = options;
|
||||
let isShuttingDown = false;
|
||||
let resolveReady: () => void = () => {};
|
||||
const ready = new Promise<void>((resolve) => {
|
||||
let resolveReady: (result: DaemonModeResult) => void = () => {};
|
||||
const ready = new Promise<DaemonModeResult>((resolve) => {
|
||||
resolveReady = resolve;
|
||||
});
|
||||
const gatewayBind = process.env.PI_GATEWAY_BIND ?? options.gateway.bind ?? "127.0.0.1";
|
||||
|
|
@ -118,7 +122,7 @@ export async function runDaemonMode(session: AgentSession, options: DaemonModeOp
|
|||
}
|
||||
|
||||
session.dispose();
|
||||
resolveReady();
|
||||
resolveReady({ reason: "shutdown" });
|
||||
};
|
||||
|
||||
const handleShutdownSignal = (signal: NodeJS.Signals) => {
|
||||
|
|
@ -126,18 +130,22 @@ export async function runDaemonMode(session: AgentSession, options: DaemonModeOp
|
|||
console.error(
|
||||
`[pi-gateway] shutdown failed for ${signal}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
resolveReady({ reason: "shutdown" });
|
||||
});
|
||||
};
|
||||
|
||||
process.once("SIGINT", () => handleShutdownSignal("SIGINT"));
|
||||
process.once("SIGTERM", () => handleShutdownSignal("SIGTERM"));
|
||||
process.once("SIGQUIT", () => handleShutdownSignal("SIGQUIT"));
|
||||
process.once("SIGHUP", () => handleShutdownSignal("SIGHUP"));
|
||||
|
||||
process.on("unhandledRejection", (error) => {
|
||||
const sigintHandler = () => handleShutdownSignal("SIGINT");
|
||||
const sigtermHandler = () => handleShutdownSignal("SIGTERM");
|
||||
const sigquitHandler = () => handleShutdownSignal("SIGQUIT");
|
||||
const sighupHandler = () => handleShutdownSignal("SIGHUP");
|
||||
const unhandledRejectionHandler = (error: unknown) => {
|
||||
console.error(`[pi-gateway] unhandled rejection: ${error instanceof Error ? error.message : String(error)}`);
|
||||
});
|
||||
};
|
||||
|
||||
process.once("SIGINT", sigintHandler);
|
||||
process.once("SIGTERM", sigtermHandler);
|
||||
process.once("SIGQUIT", sigquitHandler);
|
||||
process.once("SIGHUP", sighupHandler);
|
||||
process.on("unhandledRejection", unhandledRejectionHandler);
|
||||
|
||||
await session.bindExtensions({
|
||||
commandContextActions: createCommandContextActions(session),
|
||||
|
|
@ -146,7 +154,7 @@ export async function runDaemonMode(session: AgentSession, options: DaemonModeOp
|
|||
console.error(
|
||||
`[pi-gateway] extension shutdown failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
resolveReady({ reason: "shutdown" });
|
||||
});
|
||||
},
|
||||
onError: (err) => {
|
||||
|
|
@ -178,9 +186,19 @@ export async function runDaemonMode(session: AgentSession, options: DaemonModeOp
|
|||
const keepAlive = setInterval(() => {
|
||||
// Intentionally keep the daemon event loop active.
|
||||
}, 1000);
|
||||
ready.finally(() => {
|
||||
|
||||
const cleanup = () => {
|
||||
clearInterval(keepAlive);
|
||||
});
|
||||
await ready;
|
||||
process.exit(0);
|
||||
process.removeListener("SIGINT", sigintHandler);
|
||||
process.removeListener("SIGTERM", sigtermHandler);
|
||||
process.removeListener("SIGQUIT", sigquitHandler);
|
||||
process.removeListener("SIGHUP", sighupHandler);
|
||||
process.removeListener("unhandledRejection", unhandledRejectionHandler);
|
||||
};
|
||||
|
||||
try {
|
||||
return await ready;
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Run modes for the coding agent.
|
||||
*/
|
||||
|
||||
export { type DaemonModeOptions, runDaemonMode } from "./daemon-mode.js";
|
||||
export { type DaemonModeOptions, type DaemonModeResult, runDaemonMode } from "./daemon-mode.js";
|
||||
export { InteractiveMode, type InteractiveModeOptions } from "./interactive/interactive-mode.js";
|
||||
export { type PrintModeOptions, runPrintMode } from "./print-mode.js";
|
||||
export { type ModelInfo, RpcClient, type RpcClientOptions, type RpcEventListener } from "./rpc/rpc-client.js";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue