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
4 changes: 3 additions & 1 deletion src/adapters/channel/loadEnabledChannels.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { createLogger } from '../../pilot/logger.js';
const adapterLog = createLogger('adapters');
import type { ChannelAdapter } from "./protocol/ChannelAdapter.js";
import type { PilotAdaptersConfig, PilotPlatformAdapterConfig } from "../../pilot/config/types.js";

Expand Down Expand Up @@ -131,7 +133,7 @@ export async function loadEnabledChannels(adapters: PilotAdaptersConfig | undefi
try {
channels.push(await loader(cfg));
} catch (e) {
console.error(`[adapters] Failed to load channel "${key}": ${e}`);
adapterLog.error(`Failed to load channel "${key}"`, undefined, { channel: key, error: String(e) });
}
}

Expand Down
10 changes: 6 additions & 4 deletions src/cli/pilotdeck.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const cliLog = createLogger('cli');
import { createLogger } from '../pilot/logger.js';
#!/usr/bin/env node
import { resolve } from "node:path";
import { createAlwaysOnManager, createApplyHandler, SessionConfigOverrides, type AlwaysOnManager, type AlwaysOnConfig } from "../always-on/index.js";
Expand Down Expand Up @@ -31,15 +33,15 @@ async function main(argv = process.argv.slice(2)): Promise<void> {

const alwaysOnLogger = {
info: (message: string, data?: Record<string, unknown>) =>
console.log(`[always-on] ${message}${data ? ` ${JSON.stringify(data)}` : ""}`),
cliLog.info(message, data),
warn: (message: string, data?: Record<string, unknown>) =>
console.warn(`[always-on] ${message}${data ? ` ${JSON.stringify(data)}` : ""}`),
cliLog.warn(message, data),
};
const cronLogger = {
info: (message: string, data?: Record<string, unknown>) =>
console.log(`[cron] ${message}${data ? ` ${JSON.stringify(data)}` : ""}`),
cliLog.info(message, data),
warn: (message: string, data?: Record<string, unknown>) =>
console.warn(`[cron] ${message}${data ? ` ${JSON.stringify(data)}` : ""}`),
cliLog.warn(message, data),
};

function buildAlwaysOn(config: AlwaysOnConfig | undefined): AlwaysOnManager | undefined {
Expand Down
2 changes: 2 additions & 0 deletions src/model/providers/anthropic/stream.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { createLogger } from '../../../../pilot/logger.js';
const anthropic_stream_log = createLogger('anthropic-stream');
import { jsonrepair } from "jsonrepair";
import type { CanonicalModelEvent, CanonicalToolCall } from "../../protocol/canonical.js";
import { ModelProviderError } from "../../protocol/errors.js";
Expand Down
2 changes: 2 additions & 0 deletions src/model/providers/openai/stream.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { createLogger } from '../../../../pilot/logger.js';
const openai_stream_log = createLogger('openai-stream');
import { jsonrepair } from "jsonrepair";
import type { CanonicalModelEvent, CanonicalToolCall } from "../../protocol/canonical.js";
import { ModelProviderError } from "../../protocol/errors.js";
Expand Down
4 changes: 3 additions & 1 deletion src/model/streaming/assembleModelMessage.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { createLogger } from '../../../pilot/logger.js';
const assemble_msg_log = createLogger('assemble-msg');
import type {
CanonicalContentBlock,
CanonicalFinishReason,
Expand Down Expand Up @@ -93,7 +95,7 @@ export function assembleAssistantMessage(state: ModelMessageAssemblerState): Ass
const textBlock = state.content[textIdx] as CanonicalTextBlock;
const { toolCalls, remainingText } = extractTextToolCalls(textBlock.text);
if (toolCalls.length > 0) {
console.log(`[text-tool-call-fallback] Extracted ${toolCalls.length} tool call(s) from assistant text`);
assembleMsgLog.info("text-tool-call-fallback", { count: toolCalls.length });
if (remainingText.length > 0) {
(state.content[textIdx] as CanonicalTextBlock).text = remainingText;
} else {
Expand Down
4 changes: 3 additions & 1 deletion src/model/streaming/streamModel.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { createLogger } from '../../../pilot/logger.js';
const stream_model_log = createLogger('stream-model');
import { normalizeModelError } from "../errors/normalizeModelError.js";
import { buildModelRequest } from "../request/buildModelRequest.js";
import { validateModelRequest } from "../request/validateModelRequest.js";
Expand Down Expand Up @@ -68,7 +70,7 @@ export async function* streamModel(
const fs = await import("node:fs");
const dumpPath = `/tmp/pilotdeck_request_${Date.now()}.json`;
fs.writeFileSync(dumpPath, JSON.stringify(body, null, 2));
console.log(`[model-debug] Request dumped to ${dumpPath} (model=${currentRequest.model})`);
streamModelLog.info("model-debug request dumped", { dumpPath, model: currentRequest.model });
}
let response: Response;
try {
Expand Down
158 changes: 158 additions & 0 deletions src/pilot/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { AsyncLocalStorage } from 'node:async_hooks';

/* ------------------------------------------------------------------ */
/* Log levels */
/* ------------------------------------------------------------------ */

export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
}

/* ------------------------------------------------------------------ */
/* Log entry shape */
/* ------------------------------------------------------------------ */

export interface LogEntry {
timestamp: string;
level: LogLevel;
module: string;
message: string;
data?: Record<string, unknown>;
error?: { message: string; stack?: string };
requestId?: string;
}

/* ------------------------------------------------------------------ */
/* Request-ID context (AsyncLocalStorage) */
/* ------------------------------------------------------------------ */

export const requestContext = new AsyncLocalStorage<{ requestId: string }>();

export function getRequestId(): string {
return requestContext.getStore()?.requestId ?? 'unknown';
}

export function withRequestId<T>(requestId: string, fn: () => T): T {
return requestContext.run({ requestId }, fn);
}

export async function withRequestIdAsync<T>(
requestId: string,
fn: () => Promise<T>,
): Promise<T> {
return requestContext.run({ requestId }, fn);
}

/* ------------------------------------------------------------------ */
/* Global level control */
/* ------------------------------------------------------------------ */

let globalLevel: LogLevel | undefined;

export function setGlobalLogLevel(level: LogLevel): void {
globalLevel = level;
}

/* ------------------------------------------------------------------ */
/* Logger class */
/* ------------------------------------------------------------------ */

class PilotLogger {
private readonly module: string;

constructor(module: string) {
this.module = module;
}

/* ---- internal -------------------------------------------------- */

private shouldLog(level: LogLevel): boolean {
const effective = globalLevel ?? LogLevel.INFO;
return level >= effective;
}

private format(level: LogLevel, message: string, data?: Record<string, unknown>, error?: Error): LogEntry {
const entry: LogEntry = {
timestamp: new Date().toISOString(),
level,
module: this.module,
message,
requestId: getRequestId(),
};
if (data && Object.keys(data).length > 0) {
entry.data = data;
}
if (error) {
entry.error = { message: error.message, stack: error.stack };
}
return entry;
}

private write(entry: LogEntry): void {
const prefix = `[${entry.timestamp}] [${LogLevel[entry.level]}] [${entry.module}]`;

// Use the appropriate console method based on level.
switch (entry.level) {
case LogLevel.ERROR: {
const args: unknown[] = [prefix, entry.message];
if (entry.data) args.push(entry.data);
if (entry.error) args.push(entry.error);
console.error(...args);
break;
}
case LogLevel.WARN: {
const args: unknown[] = [prefix, entry.message];
if (entry.data) args.push(entry.data);
console.warn(...args);
break;
}
case LogLevel.DEBUG: {
const args: unknown[] = [prefix, entry.message];
if (entry.data) args.push(entry.data);
console.debug(...args);
break;
}
default: {
const args: unknown[] = [prefix, entry.message];
if (entry.data) args.push(entry.data);
console.log(...args);
break;
}
}
}

private log(level: LogLevel, message: string, data?: Record<string, unknown>, error?: Error): void {
if (!this.shouldLog(level)) return;
const entry = this.format(level, message, data, error);
this.write(entry);
}

/* ---- public API ------------------------------------------------ */

debug(message: string, data?: Record<string, unknown>): void {
this.log(LogLevel.DEBUG, message, data);
}

info(message: string, data?: Record<string, unknown>): void {
this.log(LogLevel.INFO, message, data);
}

warn(message: string, data?: Record<string, unknown>): void {
this.log(LogLevel.WARN, message, data);
}

error(message: string, error?: Error, data?: Record<string, unknown>): void {
this.log(LogLevel.ERROR, message, data, error);
}
}

/* ------------------------------------------------------------------ */
/* Factory */
/* ------------------------------------------------------------------ */

export function createLogger(module: string): PilotLogger {
return new PilotLogger(module);
}
27 changes: 19 additions & 8 deletions src/router/RouterRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {
PilotDeckCustomRouter,
CustomRouterRegistry,
} from "./customRouter/customRouter.js";
import { noopCustomRouterRegistry } from "./customRouter/customRouter.js";

import { isFallbackEligible, planFallback } from "./fallback/runFallbackChain.js";
import { applyOrchestration } from "./orchestrate/applyOrchestration.js";
import type {
Expand All @@ -36,6 +36,10 @@ import {
import { TokenStatsCollector } from "./stats/TokenStatsCollector.js";
import { classifyAndRoute } from "./tokenSaver/classifyAndRoute.js";
import { countMessagesTokens, countResponseTokens, dispose as disposeTokenizer } from "./utils/countTokens.js";
import { createLogger } from '../pilot/logger.js';
import { noopCustomRouterRegistry } from './customRouter/customRouter.js';

const routerLog = createLogger('router');

export type RouterRuntimeDeps = {
modelRuntime: ModelRuntime;
Expand Down Expand Up @@ -260,9 +264,13 @@ export function createRouterRuntime(
const alreadyOrchestrating = sticky?.orchestrating === true;
const tokenSaverActive = config.tokenSaver?.enabled === true && tokenSaverTier != null;
const orchGate = tokenSaverActive || alreadyOrchestrating;
console.log(
`[router] decision: tier=${tokenSaverTier}, model=${selection.provider}/${selection.model}, orchGate=${orchGate}, alreadyOrch=${alreadyOrchestrating}, resolvedFrom=${resolvedFrom}`,
);
routerLog.info('decision', {
tier: tokenSaverTier,
model: `${selection.provider}/${selection.model}`,
orchGate,
alreadyOrch: alreadyOrchestrating,
resolvedFrom,
});

let skillPrompt: string | undefined;
if (
Expand Down Expand Up @@ -516,10 +524,13 @@ export function createRouterRuntime(
outcome.shouldRetryZeroUsage &&
zeroUsageAttempt < zeroUsageMax
) {
console.warn(
`[PilotDeck] zeroUsageRetry: empty response from ${attempt.provider}/${attempt.model} ` +
`(attempt ${zeroUsageAttempt}/${zeroUsageMax}, session=${ctx.sessionId})`,
);
routerLog.warn('zeroUsageRetry: empty response', {
provider: attempt.provider,
model: attempt.model,
attempt: zeroUsageAttempt,
max: zeroUsageMax,
session: ctx.sessionId,
});
events.emit({
type: "pilotdeck_router_zero_usage_retry",
sessionId: ctx.sessionId,
Expand Down
4 changes: 3 additions & 1 deletion src/router/tokenSaver/classifyAndRoute.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { createLogger } from '../../../pilot/logger.js';
const tokenSaverLog = createLogger('token-saver');
import type {
CanonicalMessage,
CanonicalModelRequest,
Expand Down Expand Up @@ -91,7 +93,7 @@ export async function classifyAndRoute(
if (attempt < maxAttempts) {
continue;
}
console.warn("[token-saver] Judge returned empty after retries");
tokenSaverLog.warn("Judge returned empty after retries");
return {
tier: config.defaultTier,
selection: defaultTier.model,
Expand Down