Compare commits
2 Commits
dev
...
fix/todo-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
423a7e6aaa | ||
|
|
d5f75138dd |
@@ -77,7 +77,8 @@
|
|||||||
"edit-error-recovery",
|
"edit-error-recovery",
|
||||||
"prometheus-md-only",
|
"prometheus-md-only",
|
||||||
"start-work",
|
"start-work",
|
||||||
"sisyphus-orchestrator"
|
"sisyphus-orchestrator",
|
||||||
|
"todo-codebase-compaction"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ export const HookNameSchema = z.enum([
|
|||||||
"prometheus-md-only",
|
"prometheus-md-only",
|
||||||
"start-work",
|
"start-work",
|
||||||
"sisyphus-orchestrator",
|
"sisyphus-orchestrator",
|
||||||
|
"todo-codebase-compaction",
|
||||||
|
"usage-logging",
|
||||||
])
|
])
|
||||||
|
|
||||||
export const BuiltinCommandNameSchema = z.enum([
|
export const BuiltinCommandNameSchema = z.enum([
|
||||||
|
|||||||
@@ -29,3 +29,5 @@ export { createPrometheusMdOnlyHook } from "./prometheus-md-only";
|
|||||||
export { createTaskResumeInfoHook } from "./task-resume-info";
|
export { createTaskResumeInfoHook } from "./task-resume-info";
|
||||||
export { createStartWorkHook } from "./start-work";
|
export { createStartWorkHook } from "./start-work";
|
||||||
export { createSisyphusOrchestratorHook } from "./sisyphus-orchestrator";
|
export { createSisyphusOrchestratorHook } from "./sisyphus-orchestrator";
|
||||||
|
export { createTodoCodebaseCompactionInjector, createCustomCompactionHook } from "./todo-codebase-compaction";
|
||||||
|
export { createUsageLoggingHook } from "./usage-logging";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SummarizeContext } from "../preemptive-compaction"
|
import type { SummarizeContext, PreemptiveCompactionOptions } from "../preemptive-compaction"
|
||||||
import { injectHookMessage } from "../../features/hook-message-injector"
|
import { injectHookMessage } from "../../features/hook-message-injector"
|
||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
import { createPreemptiveCompactionHook } from "../preemptive-compaction"
|
import { createPreemptiveCompactionHook } from "../preemptive-compaction"
|
||||||
|
|||||||
229
src/hooks/usage-logging/index.ts
Normal file
229
src/hooks/usage-logging/index.ts
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import { createHash } from 'crypto';
|
||||||
|
|
||||||
|
interface LogEvent {
|
||||||
|
timestamp: string;
|
||||||
|
stack_name: string;
|
||||||
|
session_id: string;
|
||||||
|
event_type: string;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UsageLoggingOptions {
|
||||||
|
ingestUrl?: string;
|
||||||
|
stackName?: string;
|
||||||
|
batchSize?: number;
|
||||||
|
flushIntervalMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_INGEST_URL = process.env.LOG_INGEST_URL || 'http://10.100.0.20:3102/ingest';
|
||||||
|
const DEFAULT_STACK_NAME = process.env.STACK_NAME || 'unknown';
|
||||||
|
|
||||||
|
function hashContent(content: string): string {
|
||||||
|
return createHash('sha256').update(content).digest('hex').substring(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWordCount(content: string): number {
|
||||||
|
return content.split(/\s+/).filter(Boolean).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createUsageLoggingHook(options: UsageLoggingOptions = {}) {
|
||||||
|
const {
|
||||||
|
ingestUrl = DEFAULT_INGEST_URL,
|
||||||
|
stackName = DEFAULT_STACK_NAME,
|
||||||
|
batchSize = 10,
|
||||||
|
flushIntervalMs = 5000,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const enabled = process.env.USAGE_LOGGING_ENABLED !== 'false';
|
||||||
|
if (!enabled) {
|
||||||
|
return { event: async () => {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventBuffer: LogEvent[] = [];
|
||||||
|
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
const sessionStats = new Map<string, {
|
||||||
|
startTime: number;
|
||||||
|
messageCount: number;
|
||||||
|
toolUseCount: number;
|
||||||
|
tokensIn: number;
|
||||||
|
tokensOut: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
async function flushEvents(): Promise<void> {
|
||||||
|
if (eventBuffer.length === 0) return;
|
||||||
|
|
||||||
|
const eventsToSend = [...eventBuffer];
|
||||||
|
eventBuffer.length = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${ingestUrl}/batch`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(eventsToSend),
|
||||||
|
signal: AbortSignal.timeout(5000)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`[UsageLogging] Failed to send events: ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[UsageLogging] Error sending events:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queueEvent(sessionId: string, eventType: string, data: Record<string, unknown>): void {
|
||||||
|
eventBuffer.push({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
stack_name: stackName,
|
||||||
|
session_id: sessionId,
|
||||||
|
event_type: eventType,
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
if (eventBuffer.length >= batchSize) {
|
||||||
|
flushEvents();
|
||||||
|
} else if (!flushTimer) {
|
||||||
|
flushTimer = setTimeout(() => {
|
||||||
|
flushTimer = null;
|
||||||
|
flushEvents();
|
||||||
|
}, flushIntervalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrCreateSessionStats(sessionId: string) {
|
||||||
|
if (!sessionStats.has(sessionId)) {
|
||||||
|
sessionStats.set(sessionId, {
|
||||||
|
startTime: Date.now(),
|
||||||
|
messageCount: 0,
|
||||||
|
toolUseCount: 0,
|
||||||
|
tokensIn: 0,
|
||||||
|
tokensOut: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return sessionStats.get(sessionId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
event: async (input: { event: { type: string; properties?: Record<string, unknown> } }) => {
|
||||||
|
const { event } = input;
|
||||||
|
const props = event.properties || {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'session.created': {
|
||||||
|
const info = props.info as { id?: string } | undefined;
|
||||||
|
if (info?.id) {
|
||||||
|
getOrCreateSessionStats(info.id);
|
||||||
|
queueEvent(info.id, 'session_start', {
|
||||||
|
start_time: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'session.deleted': {
|
||||||
|
const info = props.info as { id?: string } | undefined;
|
||||||
|
if (info?.id) {
|
||||||
|
const stats = sessionStats.get(info.id);
|
||||||
|
if (stats) {
|
||||||
|
queueEvent(info.id, 'session_end', {
|
||||||
|
duration_ms: Date.now() - stats.startTime,
|
||||||
|
total_messages: stats.messageCount,
|
||||||
|
total_tool_uses: stats.toolUseCount,
|
||||||
|
total_tokens_in: stats.tokensIn,
|
||||||
|
total_tokens_out: stats.tokensOut
|
||||||
|
});
|
||||||
|
sessionStats.delete(info.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'message.created': {
|
||||||
|
const sessionID = props.sessionID as string | undefined;
|
||||||
|
const message = props.message as { role?: string; content?: string } | undefined;
|
||||||
|
if (sessionID && message) {
|
||||||
|
const stats = getOrCreateSessionStats(sessionID);
|
||||||
|
stats.messageCount++;
|
||||||
|
|
||||||
|
const content = typeof message.content === 'string'
|
||||||
|
? message.content
|
||||||
|
: JSON.stringify(message.content || '');
|
||||||
|
|
||||||
|
queueEvent(sessionID, 'message', {
|
||||||
|
role: message.role || 'unknown',
|
||||||
|
content_hash: hashContent(content),
|
||||||
|
content_length: content.length,
|
||||||
|
word_count: getWordCount(content),
|
||||||
|
message_number: stats.messageCount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'tool.started': {
|
||||||
|
const sessionID = props.sessionID as string | undefined;
|
||||||
|
const tool = props.tool as { name?: string } | undefined;
|
||||||
|
if (sessionID && tool?.name) {
|
||||||
|
queueEvent(sessionID, 'tool_start', {
|
||||||
|
tool: tool.name,
|
||||||
|
start_time: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'tool.completed': {
|
||||||
|
const sessionID = props.sessionID as string | undefined;
|
||||||
|
const tool = props.tool as { name?: string } | undefined;
|
||||||
|
const result = props.result as { error?: unknown } | undefined;
|
||||||
|
if (sessionID && tool?.name) {
|
||||||
|
const stats = getOrCreateSessionStats(sessionID);
|
||||||
|
stats.toolUseCount++;
|
||||||
|
|
||||||
|
queueEvent(sessionID, 'tool_use', {
|
||||||
|
tool: tool.name,
|
||||||
|
success: !result?.error,
|
||||||
|
error_message: result?.error ? String(result.error) : undefined,
|
||||||
|
tool_use_number: stats.toolUseCount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'session.error': {
|
||||||
|
const sessionID = props.sessionID as string | undefined;
|
||||||
|
const error = props.error as { message?: string; code?: string } | undefined;
|
||||||
|
if (sessionID) {
|
||||||
|
queueEvent(sessionID, 'error', {
|
||||||
|
error_message: error?.message || 'Unknown error',
|
||||||
|
error_code: error?.code
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'tokens.used': {
|
||||||
|
const sessionID = props.sessionID as string | undefined;
|
||||||
|
const usage = props.usage as { input_tokens?: number; output_tokens?: number } | undefined;
|
||||||
|
if (sessionID && usage) {
|
||||||
|
const stats = getOrCreateSessionStats(sessionID);
|
||||||
|
stats.tokensIn += usage.input_tokens || 0;
|
||||||
|
stats.tokensOut += usage.output_tokens || 0;
|
||||||
|
|
||||||
|
queueEvent(sessionID, 'tokens', {
|
||||||
|
tokens_in: usage.input_tokens,
|
||||||
|
tokens_out: usage.output_tokens,
|
||||||
|
total_tokens_in: stats.tokensIn,
|
||||||
|
total_tokens_out: stats.tokensOut
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[UsageLogging] Error processing event:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
14
src/index.ts
14
src/index.ts
@@ -31,6 +31,7 @@ import {
|
|||||||
createStartWorkHook,
|
createStartWorkHook,
|
||||||
createSisyphusOrchestratorHook,
|
createSisyphusOrchestratorHook,
|
||||||
createPrometheusMdOnlyHook,
|
createPrometheusMdOnlyHook,
|
||||||
|
createUsageLoggingHook,
|
||||||
} from "./hooks";
|
} from "./hooks";
|
||||||
import {
|
import {
|
||||||
contextCollector,
|
contextCollector,
|
||||||
@@ -146,10 +147,13 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
const compactionContextInjector = isHookEnabled("compaction-context-injector")
|
const compactionContextInjector = isHookEnabled("compaction-context-injector")
|
||||||
? createCompactionContextInjector()
|
? createCompactionContextInjector()
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const todoCodebaseCompactionInjector = isHookEnabled("todo-codebase-compaction")
|
||||||
|
? createTodoCodebaseCompactionInjector()
|
||||||
|
: undefined;
|
||||||
const preemptiveCompaction = isHookEnabled("preemptive-compaction")
|
const preemptiveCompaction = isHookEnabled("preemptive-compaction")
|
||||||
? createPreemptiveCompactionHook(ctx, {
|
? createPreemptiveCompactionHook(ctx, {
|
||||||
experimental: pluginConfig.experimental,
|
experimental: pluginConfig.experimental,
|
||||||
onBeforeSummarize: compactionContextInjector,
|
onBeforeSummarize: todoCodebaseCompactionInjector ?? compactionContextInjector,
|
||||||
getModelLimit: (providerID, modelID) =>
|
getModelLimit: (providerID, modelID) =>
|
||||||
getModelLimit(modelCacheState, providerID, modelID),
|
getModelLimit(modelCacheState, providerID, modelID),
|
||||||
})
|
})
|
||||||
@@ -209,6 +213,13 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
? createPrometheusMdOnlyHook(ctx)
|
? createPrometheusMdOnlyHook(ctx)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const usageLogging = isHookEnabled("usage-logging")
|
||||||
|
? createUsageLoggingHook({
|
||||||
|
stackName: process.env.STACK_NAME,
|
||||||
|
ingestUrl: process.env.LOG_INGEST_URL,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
const taskResumeInfo = createTaskResumeInfoHook();
|
const taskResumeInfo = createTaskResumeInfoHook();
|
||||||
|
|
||||||
const backgroundManager = new BackgroundManager(ctx);
|
const backgroundManager = new BackgroundManager(ctx);
|
||||||
@@ -408,6 +419,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
await interactiveBashSession?.event(input);
|
await interactiveBashSession?.event(input);
|
||||||
await ralphLoop?.event(input);
|
await ralphLoop?.event(input);
|
||||||
await sisyphusOrchestrator?.handler(input);
|
await sisyphusOrchestrator?.handler(input);
|
||||||
|
await usageLogging?.event(input);
|
||||||
|
|
||||||
const { event } = input;
|
const { event } = input;
|
||||||
const props = event.properties as Record<string, unknown> | undefined;
|
const props = event.properties as Record<string, unknown> | undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user