blob: 8f4727c80ded6591765d4d3598055ad88026f7da [file]
import type { SessionEvent } from '@maka/core';
import type { MakaToolContext } from './tool-runtime.js';
export const CHILD_AGENT_PROGRESS_MAX_EVENTS = 64;
export const CHILD_AGENT_PROGRESS_MAX_CHARS = 8_192;
export const CHILD_AGENT_PROGRESS_BATCH_MAX_EVENTS = 128;
export const CHILD_AGENT_PROGRESS_BATCH_MAX_CHARS = 16_384;
export interface ChildAgentProgressBudget {
readonly maxEvents: number;
readonly maxChars: number;
projectedEvents: number;
projectedChars: number;
}
export interface ChildAgentProgressProjectorOptions {
prefix?: string;
sharedBudget?: ChildAgentProgressBudget;
}
export function createChildAgentProgressBudget(
maxEvents: number,
maxChars: number,
): ChildAgentProgressBudget {
return {
maxEvents,
maxChars,
projectedEvents: 0,
projectedChars: 0,
};
}
export class ChildAgentProgressProjector {
private readonly tools = new Map<string, string>();
private readonly prefix: string;
private projectedEvents = 0;
private projectedChars = 0;
constructor(
private readonly ctx: Pick<MakaToolContext, 'emitOutput'>,
private readonly options: ChildAgentProgressProjectorOptions = {},
) {
this.prefix = options.prefix ?? 'Child';
}
observe(event: SessionEvent): void {
if (event.type === 'tool_start') {
if (!this.hasCapacity()) return;
const name = event.displayName ?? event.toolName;
this.tools.set(event.toolUseId, name);
this.emit('stdout', `${this.prefix} tool started: ${name}\n`);
return;
}
if (event.type === 'tool_result') {
const name = this.tools.get(event.toolUseId) ?? 'tool';
this.tools.delete(event.toolUseId);
this.emit(
event.isError ? 'stderr' : 'stdout',
`${this.prefix} tool ${event.isError ? 'failed' : 'finished'}: ${name}\n`,
);
return;
}
if (event.type === 'provider_retry') {
const retry =
event.phase === 'scheduled'
? `scheduled: attempt ${event.attempt}/${event.maxAttempts} in ${event.delayMs}ms`
: `started: attempt ${event.attempt}/${event.maxAttempts}`;
this.emit('stderr', `${this.prefix} provider retry ${retry} (${event.reason})\n`);
}
}
private emit(stream: 'stdout' | 'stderr', chunk: string): void {
if (!this.hasCapacity()) return;
const shared = this.options.sharedBudget;
const localRemaining = CHILD_AGENT_PROGRESS_MAX_CHARS - this.projectedChars;
const sharedRemaining = shared ? shared.maxChars - shared.projectedChars : localRemaining;
const remaining = Math.min(localRemaining, sharedRemaining);
const bounded = chunk.slice(0, remaining);
this.projectedEvents += 1;
this.projectedChars += bounded.length;
if (shared) {
shared.projectedEvents += 1;
shared.projectedChars += bounded.length;
}
this.ctx.emitOutput(stream, bounded);
}
private hasCapacity(): boolean {
const shared = this.options.sharedBudget;
return (
this.projectedEvents < CHILD_AGENT_PROGRESS_MAX_EVENTS &&
this.projectedChars < CHILD_AGENT_PROGRESS_MAX_CHARS &&
(!shared ||
(shared.projectedEvents < shared.maxEvents && shared.projectedChars < shared.maxChars))
);
}
}