mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-30 00:18:40 +02:00
feat: emit tool-call file previews
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1735,6 +1735,8 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
sourceAgentId: event.sourceAgentId,
|
||||
sourceAgentName: event.sourceAgentName,
|
||||
toolName: event.toolName,
|
||||
toolCallId: event.toolCallId,
|
||||
fileChanges: event.fileChanges,
|
||||
}));
|
||||
}
|
||||
if (nextRun) {
|
||||
@@ -1753,6 +1755,8 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
sourceAgentId: event.sourceAgentId,
|
||||
sourceAgentName: event.sourceAgentName,
|
||||
toolName: event.toolName,
|
||||
toolCallId: event.toolCallId,
|
||||
fileChanges: event.fileChanges,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -241,6 +241,12 @@ export interface TurnCompleteEvent {
|
||||
|
||||
export type AgentActivityType = 'thinking' | 'tool-calling' | 'handoff' | 'completed';
|
||||
|
||||
export interface ToolCallFileChangePreview {
|
||||
path: string;
|
||||
diff?: string;
|
||||
newFileContents?: string;
|
||||
}
|
||||
|
||||
export interface AgentActivityEvent {
|
||||
type: 'agent-activity';
|
||||
requestId: string;
|
||||
@@ -251,6 +257,8 @@ export interface AgentActivityEvent {
|
||||
sourceAgentId?: string;
|
||||
sourceAgentName?: string;
|
||||
toolName?: string;
|
||||
toolCallId?: string;
|
||||
fileChanges?: ToolCallFileChangePreview[];
|
||||
}
|
||||
|
||||
export type SubagentEventKind = 'started' | 'completed' | 'failed' | 'selected' | 'deselected';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SessionRunRecord } from '@shared/domain/runTimeline';
|
||||
|
||||
import type { QuotaSnapshot } from '@shared/contracts/sidecar';
|
||||
import type { QuotaSnapshot, ToolCallFileChangePreview } from '@shared/contracts/sidecar';
|
||||
|
||||
export type SessionActivityType = 'thinking' | 'tool-calling' | 'handoff' | 'completed';
|
||||
|
||||
@@ -36,6 +36,8 @@ export interface SessionEventRecord {
|
||||
sourceAgentId?: string;
|
||||
sourceAgentName?: string;
|
||||
toolName?: string;
|
||||
toolCallId?: string;
|
||||
fileChanges?: ToolCallFileChangePreview[];
|
||||
run?: SessionRunRecord;
|
||||
error?: string;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
ApprovalDecision,
|
||||
PendingApprovalRecord,
|
||||
} from '@shared/domain/approval';
|
||||
import type { ToolCallFileChangePreview } from '@shared/contracts/sidecar';
|
||||
import type { PatternDefinition, ReasoningEffort } from '@shared/domain/pattern';
|
||||
import type { ProjectRecord } from '@shared/domain/project';
|
||||
import { createId } from '@shared/utils/ids';
|
||||
@@ -41,6 +42,8 @@ export interface RunTimelineEventRecord {
|
||||
targetAgentId?: string;
|
||||
targetAgentName?: string;
|
||||
toolName?: string;
|
||||
toolCallId?: string;
|
||||
fileChanges?: ToolCallFileChangePreview[];
|
||||
approvalId?: string;
|
||||
approvalKind?: ApprovalCheckpointKind;
|
||||
approvalTitle?: string;
|
||||
@@ -86,6 +89,8 @@ export interface AppendRunActivityEventInput {
|
||||
sourceAgentId?: string;
|
||||
sourceAgentName?: string;
|
||||
toolName?: string;
|
||||
toolCallId?: string;
|
||||
fileChanges?: ToolCallFileChangePreview[];
|
||||
}
|
||||
|
||||
export interface UpsertRunMessageEventInput {
|
||||
@@ -113,6 +118,87 @@ function normalizeOptionalString(value: string | undefined): string | undefined
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function normalizeOptionalPreviewText(value: string | undefined): string | undefined {
|
||||
return value?.trim() ? value : undefined;
|
||||
}
|
||||
|
||||
function normalizeToolCallFileChange(
|
||||
change: ToolCallFileChangePreview,
|
||||
): ToolCallFileChangePreview | undefined {
|
||||
const path = normalizeOptionalString(change.path);
|
||||
if (!path) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const diff = normalizeOptionalPreviewText(change.diff);
|
||||
const newFileContents = normalizeOptionalPreviewText(change.newFileContents);
|
||||
return {
|
||||
path,
|
||||
diff,
|
||||
newFileContents,
|
||||
};
|
||||
}
|
||||
|
||||
function mergeToolCallFileChange(
|
||||
existing: ToolCallFileChangePreview,
|
||||
incoming: ToolCallFileChangePreview,
|
||||
): ToolCallFileChangePreview {
|
||||
return {
|
||||
path: incoming.path,
|
||||
diff: incoming.diff ?? existing.diff,
|
||||
newFileContents: incoming.newFileContents ?? existing.newFileContents,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeToolCallFileChanges(
|
||||
changes: readonly ToolCallFileChangePreview[] | undefined,
|
||||
): ToolCallFileChangePreview[] | undefined {
|
||||
if (!changes || changes.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalized = new Map<string, ToolCallFileChangePreview>();
|
||||
for (const change of changes) {
|
||||
const nextChange = normalizeToolCallFileChange(change);
|
||||
if (!nextChange) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const previous = normalized.get(nextChange.path);
|
||||
normalized.set(
|
||||
nextChange.path,
|
||||
previous ? mergeToolCallFileChange(previous, nextChange) : nextChange,
|
||||
);
|
||||
}
|
||||
|
||||
return normalized.size > 0 ? [...normalized.values()] : undefined;
|
||||
}
|
||||
|
||||
function mergeToolCallFileChanges(
|
||||
existing: readonly ToolCallFileChangePreview[] | undefined,
|
||||
incoming: readonly ToolCallFileChangePreview[] | undefined,
|
||||
): ToolCallFileChangePreview[] | undefined {
|
||||
const normalizedExisting = normalizeToolCallFileChanges(existing);
|
||||
const normalizedIncoming = normalizeToolCallFileChanges(incoming);
|
||||
if (!normalizedExisting) {
|
||||
return normalizedIncoming;
|
||||
}
|
||||
|
||||
if (!normalizedIncoming) {
|
||||
return normalizedExisting;
|
||||
}
|
||||
|
||||
const merged = new Map(
|
||||
normalizedExisting.map((change) => [change.path, change] satisfies [string, ToolCallFileChangePreview]),
|
||||
);
|
||||
for (const change of normalizedIncoming) {
|
||||
const previous = merged.get(change.path);
|
||||
merged.set(change.path, previous ? mergeToolCallFileChange(previous, change) : change);
|
||||
}
|
||||
|
||||
return [...merged.values()];
|
||||
}
|
||||
|
||||
function normalizeRunTimelineAgent(
|
||||
agent: RunTimelineAgentRecord,
|
||||
): RunTimelineAgentRecord | undefined {
|
||||
@@ -153,6 +239,8 @@ function normalizeRunTimelineEvent(
|
||||
targetAgentId: normalizeOptionalString(event.targetAgentId),
|
||||
targetAgentName: normalizeOptionalString(event.targetAgentName),
|
||||
toolName: normalizeOptionalString(event.toolName),
|
||||
toolCallId: normalizeOptionalString(event.toolCallId),
|
||||
fileChanges: normalizeToolCallFileChanges(event.fileChanges),
|
||||
approvalId: normalizeOptionalString(event.approvalId),
|
||||
approvalKind: event.approvalKind,
|
||||
approvalTitle: normalizeOptionalString(event.approvalTitle),
|
||||
@@ -217,6 +305,28 @@ function appendRunTimelineEvent(
|
||||
};
|
||||
}
|
||||
|
||||
function upsertRunTimelineEventAt(
|
||||
run: SessionRunRecord,
|
||||
eventIndex: number,
|
||||
event: RunTimelineEventRecord,
|
||||
): SessionRunRecord {
|
||||
if (eventIndex < 0 || eventIndex >= run.events.length) {
|
||||
return appendRunTimelineEvent(run, event);
|
||||
}
|
||||
|
||||
const nextEvent = normalizeRunTimelineEvent(event);
|
||||
if (!nextEvent) {
|
||||
return run;
|
||||
}
|
||||
|
||||
const nextEvents = run.events.slice();
|
||||
nextEvents[eventIndex] = nextEvent;
|
||||
return {
|
||||
...run,
|
||||
events: nextEvents,
|
||||
};
|
||||
}
|
||||
|
||||
function settleOpenMessageEvents(
|
||||
run: SessionRunRecord,
|
||||
status: Extract<RunTimelineEventStatus, 'completed' | 'error'>,
|
||||
@@ -417,13 +527,26 @@ export function appendRunActivityEvent(
|
||||
}
|
||||
case 'tool-calling': {
|
||||
const agent = resolveRunTimelineAgent(run, input.agentId, input.agentName);
|
||||
return appendRunTimelineEvent(run, {
|
||||
const toolCallId = normalizeOptionalString(input.toolCallId);
|
||||
const existingIndex = toolCallId
|
||||
? run.events.findIndex((event) => event.kind === 'tool-call' && event.toolCallId === toolCallId)
|
||||
: -1;
|
||||
const existingEvent = existingIndex >= 0 ? run.events[existingIndex] : undefined;
|
||||
const nextEvent: RunTimelineEventRecord = {
|
||||
id: existingEvent?.id ?? createId('run-event'),
|
||||
kind: 'tool-call',
|
||||
occurredAt: input.occurredAt,
|
||||
occurredAt: existingEvent?.occurredAt ?? input.occurredAt,
|
||||
updatedAt: existingEvent ? input.occurredAt : undefined,
|
||||
status: 'completed',
|
||||
...agent,
|
||||
toolName: normalizeOptionalString(input.toolName),
|
||||
});
|
||||
agentId: agent.agentId ?? existingEvent?.agentId,
|
||||
agentName: agent.agentName ?? existingEvent?.agentName,
|
||||
toolName: normalizeOptionalString(input.toolName) ?? existingEvent?.toolName,
|
||||
toolCallId,
|
||||
fileChanges: mergeToolCallFileChanges(existingEvent?.fileChanges, input.fileChanges),
|
||||
};
|
||||
return existingIndex >= 0
|
||||
? upsertRunTimelineEventAt(run, existingIndex, nextEvent)
|
||||
: appendRunTimelineEvent(run, nextEvent);
|
||||
}
|
||||
case 'handoff': {
|
||||
const sourceAgent = resolveRunTimelineAgent(run, input.sourceAgentId, input.sourceAgentName);
|
||||
|
||||
Reference in New Issue
Block a user