[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
317
infrastructure/ai/harness/agentEventAdapter.ts
Normal file
317
infrastructure/ai/harness/agentEventAdapter.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
import type { AgentEvent, AgentEventListener } from './types';
|
||||
import { resolveStreamChunkToolCallId } from '../aiChatStreamingSupport';
|
||||
|
||||
let eventCounter = 0;
|
||||
|
||||
function nextEventId(prefix: string): string {
|
||||
eventCounter += 1;
|
||||
return `${prefix}-${Date.now()}-${eventCounter}`;
|
||||
}
|
||||
|
||||
export interface StreamEventContext {
|
||||
sessionId: string;
|
||||
chatSessionId?: string;
|
||||
turnId?: string;
|
||||
}
|
||||
|
||||
export interface CattyStreamChunk {
|
||||
type: string;
|
||||
text?: string;
|
||||
textDelta?: string;
|
||||
delta?: string;
|
||||
toolCallId?: string;
|
||||
toolName?: string;
|
||||
input?: unknown;
|
||||
args?: unknown;
|
||||
output?: unknown;
|
||||
result?: unknown;
|
||||
error?: unknown;
|
||||
approved?: boolean;
|
||||
stepNumber?: number;
|
||||
}
|
||||
|
||||
const STEP_HANDLE_NOTICE_RE = /^\[step \d+\] Tool output handles available:/;
|
||||
|
||||
function isStepHandleNoticeMessage(content: unknown): boolean {
|
||||
return typeof content === 'string' && STEP_HANDLE_NOTICE_RE.test(content);
|
||||
}
|
||||
|
||||
function isToolResultError(output: unknown): boolean {
|
||||
if (output == null) return false;
|
||||
if (typeof output === 'object') {
|
||||
const obj = output as Record<string, unknown>;
|
||||
if ('error' in obj && typeof obj.error === 'string') return true;
|
||||
if ('ok' in obj && obj.ok === false) return true;
|
||||
}
|
||||
if (typeof output === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(output) as Record<string, unknown>;
|
||||
if ('error' in parsed && typeof parsed.error === 'string') return true;
|
||||
if ('ok' in parsed && parsed.ok === false) return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function mapSdkStreamEventToAgentEvents(
|
||||
event: Record<string, unknown>,
|
||||
ctx: StreamEventContext,
|
||||
): AgentEvent[] {
|
||||
const base = {
|
||||
sessionId: ctx.sessionId,
|
||||
chatSessionId: ctx.chatSessionId,
|
||||
backend: 'external-sdk' as const,
|
||||
timestamp: Date.now(),
|
||||
turnId: ctx.turnId,
|
||||
};
|
||||
|
||||
switch (event.type) {
|
||||
case 'text-delta':
|
||||
return [{
|
||||
...base,
|
||||
id: nextEventId('model-delta'),
|
||||
type: 'model_delta',
|
||||
text: String(event.text ?? event.textDelta ?? event.delta ?? ''),
|
||||
}];
|
||||
case 'thinking-delta':
|
||||
case 'reasoning-delta':
|
||||
return [{
|
||||
...base,
|
||||
id: nextEventId('reasoning-delta'),
|
||||
type: 'reasoning_delta',
|
||||
text: String(event.text ?? event.textDelta ?? event.delta ?? ''),
|
||||
}];
|
||||
case 'tool-call':
|
||||
return [{
|
||||
...base,
|
||||
id: nextEventId('tool-call'),
|
||||
type: 'tool_call',
|
||||
toolCallId: String(event.toolCallId ?? event.id ?? ''),
|
||||
toolName: String(event.toolName ?? event.name ?? 'unknown'),
|
||||
args: (event.args ?? event.input ?? {}) as Record<string, unknown>,
|
||||
}];
|
||||
case 'tool-result':
|
||||
{
|
||||
const output = event.result ?? event.output ?? '';
|
||||
return [{
|
||||
...base,
|
||||
id: nextEventId('tool-result'),
|
||||
type: 'tool_result',
|
||||
toolCallId: String(event.toolCallId ?? ''),
|
||||
toolName: typeof event.toolName === 'string' ? event.toolName : undefined,
|
||||
result: typeof output === 'string' ? output : JSON.stringify(output),
|
||||
isError: Boolean(event.isError) || isToolResultError(output),
|
||||
}];
|
||||
}
|
||||
case 'file-change':
|
||||
return [{
|
||||
...base,
|
||||
id: nextEventId('file-change'),
|
||||
type: 'file_change',
|
||||
itemId: String(event.itemId ?? ''),
|
||||
status: event.status === 'failed' ? 'failed' : 'completed',
|
||||
changes: Array.isArray(event.changes)
|
||||
? event.changes as Array<{ path: string; kind: 'add' | 'delete' | 'update' }>
|
||||
: [],
|
||||
}];
|
||||
case 'web-search':
|
||||
return [{
|
||||
...base,
|
||||
id: nextEventId('web-search'),
|
||||
type: 'web_search',
|
||||
itemId: String(event.itemId ?? ''),
|
||||
query: String(event.query ?? ''),
|
||||
status: event.status === 'completed' ? 'completed' : 'running',
|
||||
}];
|
||||
case 'plan-update':
|
||||
return [{
|
||||
...base,
|
||||
id: nextEventId('plan-update'),
|
||||
type: 'plan_update',
|
||||
itemId: String(event.itemId ?? ''),
|
||||
status: event.status === 'completed' ? 'completed' : 'running',
|
||||
items: Array.isArray(event.items)
|
||||
? event.items as Array<{ text: string; completed: boolean }>
|
||||
: [],
|
||||
}];
|
||||
case 'warning':
|
||||
return [{
|
||||
...base,
|
||||
id: nextEventId('warning'),
|
||||
type: 'error',
|
||||
message: String(event.message ?? 'Unknown SDK warning'),
|
||||
recoverable: true,
|
||||
}];
|
||||
case 'usage': {
|
||||
const promptTokens = Number(event.inputTokens) || 0;
|
||||
const completionTokens = Number(event.outputTokens) || 0;
|
||||
return [{
|
||||
...base,
|
||||
id: nextEventId('usage'),
|
||||
type: 'usage',
|
||||
promptTokens,
|
||||
cachedPromptTokens: Number(event.cachedInputTokens) || 0,
|
||||
completionTokens,
|
||||
reasoningTokens: Number(event.reasoningTokens) || 0,
|
||||
totalTokens: Number(event.totalTokens) || promptTokens + completionTokens,
|
||||
estimated: false,
|
||||
}];
|
||||
}
|
||||
case 'error':
|
||||
return [{
|
||||
...base,
|
||||
id: nextEventId('error'),
|
||||
type: 'error',
|
||||
message: String(event.error ?? event.message ?? 'Unknown SDK error'),
|
||||
recoverable: false,
|
||||
}];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function mapCattyStreamChunkToAgentEvents(
|
||||
chunk: CattyStreamChunk,
|
||||
ctx: StreamEventContext,
|
||||
): AgentEvent[] {
|
||||
const base = {
|
||||
sessionId: ctx.sessionId,
|
||||
chatSessionId: ctx.chatSessionId,
|
||||
backend: 'catty' as const,
|
||||
timestamp: Date.now(),
|
||||
turnId: ctx.turnId,
|
||||
};
|
||||
|
||||
if (chunk.type === 'text' || chunk.type === 'text-delta') {
|
||||
const text = chunk.text ?? chunk.textDelta ?? '';
|
||||
if (!text) return [];
|
||||
return [{ ...base, id: nextEventId('model-delta'), type: 'model_delta', text }];
|
||||
}
|
||||
|
||||
if (chunk.type === 'reasoning' || chunk.type === 'reasoning-start' || chunk.type === 'reasoning-delta') {
|
||||
const text = chunk.text ?? chunk.textDelta ?? chunk.delta ?? '';
|
||||
if (!text) return [];
|
||||
return [{ ...base, id: nextEventId('reasoning-delta'), type: 'reasoning_delta', text }];
|
||||
}
|
||||
|
||||
if (chunk.type === 'tool-call' && chunk.toolCallId && chunk.toolName) {
|
||||
return [{
|
||||
...base,
|
||||
id: nextEventId('tool-call'),
|
||||
type: 'tool_call',
|
||||
toolCallId: chunk.toolCallId,
|
||||
toolName: chunk.toolName,
|
||||
args: (chunk.input ?? chunk.args ?? {}) as Record<string, unknown>,
|
||||
}];
|
||||
}
|
||||
|
||||
if (chunk.type === 'tool-result' && chunk.toolCallId) {
|
||||
const output = chunk.output ?? chunk.result;
|
||||
const resultText = typeof output === 'string' ? output : JSON.stringify(output ?? '');
|
||||
return [{
|
||||
...base,
|
||||
id: nextEventId('tool-result'),
|
||||
type: 'tool_result',
|
||||
toolCallId: chunk.toolCallId,
|
||||
toolName: typeof chunk.toolName === 'string' ? chunk.toolName : undefined,
|
||||
result: resultText,
|
||||
isError: isToolResultError(output),
|
||||
}];
|
||||
}
|
||||
|
||||
if (chunk.type === 'tool-error' && chunk.toolCallId) {
|
||||
const resultText = chunk.error instanceof Error
|
||||
? JSON.stringify({ error: chunk.error.message })
|
||||
: typeof chunk.error === 'string'
|
||||
? JSON.stringify({ error: chunk.error })
|
||||
: JSON.stringify({ error: String(chunk.error ?? 'Tool execution failed.') });
|
||||
return [{
|
||||
...base,
|
||||
id: nextEventId('tool-result'),
|
||||
type: 'tool_result',
|
||||
toolCallId: chunk.toolCallId,
|
||||
toolName: typeof chunk.toolName === 'string' ? chunk.toolName : undefined,
|
||||
result: resultText,
|
||||
isError: true,
|
||||
}];
|
||||
}
|
||||
|
||||
if (chunk.type === 'error') {
|
||||
const message = chunk.error instanceof Error
|
||||
? chunk.error.message
|
||||
: String(chunk.error ?? 'Unknown stream error');
|
||||
return [{ ...base, id: nextEventId('error'), type: 'error', message }];
|
||||
}
|
||||
|
||||
if (chunk.type === 'tool-approval-request') {
|
||||
const toolCallId = resolveStreamChunkToolCallId(chunk);
|
||||
const toolName = chunk.toolName ?? chunk.toolCall?.toolName;
|
||||
if (!toolCallId || !toolName) return [];
|
||||
return [{
|
||||
...base,
|
||||
id: nextEventId('approval-requested'),
|
||||
type: 'approval_requested',
|
||||
toolCallId,
|
||||
toolName,
|
||||
args: (chunk.input ?? chunk.args ?? chunk.toolCall?.input ?? {}) as Record<string, unknown>,
|
||||
}];
|
||||
}
|
||||
|
||||
if (chunk.type === 'tool-approval-response') {
|
||||
const toolCallId = resolveStreamChunkToolCallId(chunk);
|
||||
if (!toolCallId) return [];
|
||||
const approved = chunk.approved === true;
|
||||
const events: AgentEvent[] = [{
|
||||
...base,
|
||||
id: nextEventId('approval-resolved'),
|
||||
type: 'approval_resolved',
|
||||
toolCallId,
|
||||
toolName: String(chunk.toolName ?? chunk.toolCall?.toolName ?? 'unknown'),
|
||||
outcome: approved ? 'approved' : 'denied',
|
||||
}];
|
||||
if (!approved) {
|
||||
events.push({
|
||||
...base,
|
||||
id: nextEventId('tool-result'),
|
||||
type: 'tool_result',
|
||||
toolCallId,
|
||||
result: JSON.stringify({ error: chunk.reason ?? 'Tool execution denied.' }),
|
||||
isError: true,
|
||||
});
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
if (chunk.type === 'tool-output-denied' && chunk.toolCallId) {
|
||||
return [
|
||||
{
|
||||
...base,
|
||||
id: nextEventId('approval-resolved'),
|
||||
type: 'approval_resolved',
|
||||
toolCallId: chunk.toolCallId,
|
||||
toolName: String(chunk.toolName ?? 'unknown'),
|
||||
outcome: 'denied',
|
||||
},
|
||||
{
|
||||
...base,
|
||||
id: nextEventId('tool-result'),
|
||||
type: 'tool_result',
|
||||
toolCallId: chunk.toolCallId,
|
||||
result: JSON.stringify({ error: 'Tool execution denied.' }),
|
||||
isError: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export { isStepHandleNoticeMessage };
|
||||
|
||||
export function createHarnessEventSink(
|
||||
listener: AgentEventListener,
|
||||
): AgentEventListener {
|
||||
return (event) => listener(event);
|
||||
}
|
||||
Reference in New Issue
Block a user