Skip to content

Commit 0cb64b8

Browse files
committed
feat(ipc): thread generationId through agent:event:v1 for end-to-end trace
1 parent 79348ab commit 0cb64b8

5 files changed

Lines changed: 173 additions & 18 deletions

File tree

apps/desktop/src/main/index.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ function registerIpcHandlers(): void {
131131
* sink the IPC handler uses. Keeps a single timeline per generation in the
132132
* log file without forcing `core` to depend on electron-log. */
133133
const coreLoggerFor = (id: string): CoreLogger => ({
134-
info: (event, data) => logIpc.info(event, { id, ...(data ?? {}) }),
135-
error: (event, data) => logIpc.error(event, { id, ...(data ?? {}) }),
134+
info: (event, data) => logIpc.info(event, { generationId: id, ...(data ?? {}) }),
135+
error: (event, data) => logIpc.error(event, { generationId: id, ...(data ?? {}) }),
136136
});
137137

138138
/**
@@ -246,23 +246,23 @@ function registerIpcHandlers(): void {
246246
if (event.type === 'turn_start') {
247247
deltaCount = 0;
248248
toolCount = 0;
249-
logIpc.info('agent.turn_start', { id });
249+
logIpc.info('agent.turn_start', { generationId: id });
250250
} else if (event.type === 'message_update') {
251251
const ame = event.assistantMessageEvent;
252252
if (ame.type === 'text_delta') deltaCount += 1;
253253
} else if (event.type === 'tool_execution_start') {
254254
toolCount += 1;
255-
logIpc.info('agent.tool_start', { id, tool: event.toolName });
255+
logIpc.info('agent.tool_start', { generationId: id, tool: event.toolName });
256256
} else if (event.type === 'tool_execution_end') {
257257
logIpc.info('agent.tool_end', {
258-
id,
258+
generationId: id,
259259
tool: event.toolName,
260260
isError: event.isError,
261261
});
262262
} else if (event.type === 'turn_end') {
263-
logIpc.info('agent.turn_end', { id, deltas: deltaCount, tools: toolCount });
263+
logIpc.info('agent.turn_end', { generationId: id, deltas: deltaCount, tools: toolCount });
264264
} else if (event.type === 'agent_end') {
265-
logIpc.info('agent.end', { id });
265+
logIpc.info('agent.end', { generationId: id });
266266
}
267267
if (designId === null) return; // no routing target
268268
if (event.type === 'turn_start') {
@@ -475,7 +475,7 @@ function registerIpcHandlers(): void {
475475
}
476476

477477
const stepCtx = {
478-
id,
478+
generationId: id,
479479
provider: active.model.provider,
480480
modelId: active.model.modelId,
481481
};
@@ -500,7 +500,7 @@ function registerIpcHandlers(): void {
500500
});
501501

502502
logIpc.info('generate', {
503-
id,
503+
generationId: id,
504504
provider: active.model.provider,
505505
modelId: active.model.modelId,
506506
...(active.overridden
@@ -539,15 +539,15 @@ function registerIpcHandlers(): void {
539539
payload.previousHtml ?? null,
540540
);
541541
logIpc.info('generate.ok', {
542-
id,
542+
generationId: id,
543543
ms: Date.now() - t0,
544544
artifacts: result.artifacts.length,
545545
cost: result.costUsd,
546546
});
547547
return result;
548548
} catch (err) {
549549
logIpc.error('generate.fail', {
550-
id,
550+
generationId: id,
551551
ms: Date.now() - t0,
552552
provider: active.model.provider,
553553
modelId: active.model.modelId,
@@ -601,7 +601,7 @@ function registerIpcHandlers(): void {
601601
});
602602

603603
logIpc.info('generate', {
604-
id,
604+
generationId: id,
605605
provider: active.model.provider,
606606
modelId: active.model.modelId,
607607
...(active.overridden
@@ -639,15 +639,15 @@ function registerIpcHandlers(): void {
639639
null,
640640
);
641641
logIpc.info('generate.ok', {
642-
id,
642+
generationId: id,
643643
ms: Date.now() - t0,
644644
artifacts: result.artifacts.length,
645645
cost: result.costUsd,
646646
});
647647
return result;
648648
} catch (err) {
649649
logIpc.error('generate.fail', {
650-
id,
650+
generationId: id,
651651
ms: Date.now() - t0,
652652
provider: active.model.provider,
653653
modelId: active.model.modelId,

apps/desktop/src/preload/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,10 @@ export interface AgentStreamEvent {
127127
| 'agent_end'
128128
| 'error';
129129
designId: string;
130-
generationId?: string;
130+
/** Trace ID linking this event to the main-process generation log entry.
131+
* Matches the generationId from the codesign:v1:generate payload — always
132+
* present because the main process supplies it from baseCtx. */
133+
generationId: string;
131134
// turn_start
132135
turnId?: string;
133136
// text_delta
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/**
2+
* Verifies that agent:event:v1 payloads carry generationId through to log
3+
* payloads. The handlers in useAgentStream extract event.generationId into
4+
* console.debug calls — this test exercises the extraction logic in isolation
5+
* without needing a React renderer or Electron IPC.
6+
*/
7+
8+
import { describe, expect, it } from 'vitest';
9+
import type { AgentStreamEvent } from '../../../../preload/index';
10+
11+
interface LogPayload {
12+
generationId: string;
13+
designId: string;
14+
textLen?: number | undefined;
15+
message?: string | undefined;
16+
code?: string | undefined;
17+
toolName?: string | undefined;
18+
toolCallId?: string | undefined;
19+
}
20+
21+
/** Simulates the log-payload extraction performed by handleTurnStart. */
22+
function turnStartLogPayload(event: AgentStreamEvent): LogPayload {
23+
return { generationId: event.generationId, designId: event.designId };
24+
}
25+
26+
/** Simulates the log-payload extraction performed by handleTurnEnd. */
27+
function turnEndLogPayload(event: AgentStreamEvent, textBuffer: string): LogPayload {
28+
return {
29+
generationId: event.generationId,
30+
designId: event.designId,
31+
textLen: (event.finalText ?? textBuffer).length,
32+
};
33+
}
34+
35+
/** Simulates the log-payload extraction performed by handleError. */
36+
function errorLogPayload(event: AgentStreamEvent): LogPayload {
37+
return {
38+
generationId: event.generationId,
39+
designId: event.designId,
40+
message: event.message,
41+
code: event.code,
42+
};
43+
}
44+
45+
/** Simulates the log-payload extraction performed by handleAgentEnd. */
46+
function agentEndLogPayload(event: AgentStreamEvent): LogPayload {
47+
return { generationId: event.generationId, designId: event.designId };
48+
}
49+
50+
/** Simulates the log-payload extraction performed by handleToolCallStart. */
51+
function toolCallStartLogPayload(event: AgentStreamEvent): LogPayload {
52+
return {
53+
generationId: event.generationId,
54+
designId: event.designId,
55+
toolName: event.toolName ?? 'unknown',
56+
toolCallId: event.toolCallId,
57+
};
58+
}
59+
60+
describe('useAgentStream — generationId in log payloads', () => {
61+
const GEN_ID = 'lf3a2k-xyz9';
62+
const DESIGN_ID = 'design-001';
63+
64+
const baseEvent = (
65+
type: AgentStreamEvent['type'],
66+
extra: Partial<AgentStreamEvent> = {},
67+
): AgentStreamEvent => ({
68+
type,
69+
designId: DESIGN_ID,
70+
generationId: GEN_ID,
71+
...extra,
72+
});
73+
74+
it('turn_start log carries generationId', () => {
75+
const payload = turnStartLogPayload(baseEvent('turn_start'));
76+
expect(payload.generationId).toBe(GEN_ID);
77+
expect(payload.designId).toBe(DESIGN_ID);
78+
});
79+
80+
it('turn_end log carries generationId and textLen', () => {
81+
const payload = turnEndLogPayload(baseEvent('turn_end', { finalText: 'hello' }), '');
82+
expect(payload.generationId).toBe(GEN_ID);
83+
expect(payload.textLen).toBe(5);
84+
});
85+
86+
it('turn_end falls back to textBuffer when finalText absent', () => {
87+
const payload = turnEndLogPayload(baseEvent('turn_end'), 'buffered text');
88+
expect(payload.generationId).toBe(GEN_ID);
89+
expect(payload.textLen).toBe('buffered text'.length);
90+
});
91+
92+
it('error log carries generationId, message, code', () => {
93+
const payload = errorLogPayload(
94+
baseEvent('error', { message: 'timeout', code: 'GENERATION_TIMEOUT' }),
95+
);
96+
expect(payload.generationId).toBe(GEN_ID);
97+
expect(payload.message).toBe('timeout');
98+
expect(payload.code).toBe('GENERATION_TIMEOUT');
99+
});
100+
101+
it('agent_end log carries generationId', () => {
102+
const payload = agentEndLogPayload(baseEvent('agent_end'));
103+
expect(payload.generationId).toBe(GEN_ID);
104+
});
105+
106+
it('tool_call_start log carries generationId and toolName', () => {
107+
const payload = toolCallStartLogPayload(
108+
baseEvent('tool_call_start', { toolName: 'str_replace', toolCallId: 'tc-1' }),
109+
);
110+
expect(payload.generationId).toBe(GEN_ID);
111+
expect(payload.toolName).toBe('str_replace');
112+
expect(payload.toolCallId).toBe('tc-1');
113+
});
114+
115+
it('AgentStreamEvent.generationId is a non-empty string', () => {
116+
// Verifies the type contract: generationId: string (required, not undefined).
117+
const event: AgentStreamEvent = {
118+
type: 'turn_start',
119+
designId: DESIGN_ID,
120+
generationId: GEN_ID,
121+
};
122+
expect(typeof event.generationId).toBe('string');
123+
expect(event.generationId.length).toBeGreaterThan(0);
124+
});
125+
});

apps/desktop/src/renderer/src/hooks/useAgentStream.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ interface PendingPersist {
2626

2727
interface InFlightTurn {
2828
designId: string;
29-
generationId: string | undefined;
29+
/** Matches the generationId from agent:event:v1 — guaranteed non-empty since
30+
* AgentStreamEvent.generationId is required as of schema v1. */
31+
generationId: string;
3032
textBuffer: string;
3133
/** Final assistant text persisted on the previous turn_end of this run.
3234
* pi-agent-core can re-emit the same trailing assistant prose across
@@ -84,6 +86,8 @@ export function useAgentStream(): void {
8486
};
8587

8688
const handleTurnStart = (event: AgentStreamEvent) => {
89+
// TODO: replace with rendererLogger once renderer-logger lands
90+
console.debug('[agent] turn_start', { generationId: event.generationId, designId: event.designId });
8791
const previous = inFlight.current;
8892
const sameRun =
8993
previous &&
@@ -123,6 +127,12 @@ export function useAgentStream(): void {
123127

124128
const handleTurnEnd = (event: AgentStreamEvent) => {
125129
const current = inFlight.current;
130+
// TODO: replace with rendererLogger once renderer-logger lands
131+
console.debug('[agent] turn_end', {
132+
generationId: event.generationId,
133+
designId: event.designId,
134+
textLen: (event.finalText ?? current?.textBuffer ?? '').length,
135+
});
126136
const finalText = event.finalText ?? current?.textBuffer ?? '';
127137
const trimmed = finalText.trim();
128138
if (current && trimmed.length > 0 && trimmed !== current.lastPersistedText?.trim()) {
@@ -142,7 +152,13 @@ export function useAgentStream(): void {
142152
const current = inFlight.current;
143153
const designId = event.designId;
144154
const toolName = event.toolName ?? 'unknown';
145-
// Persist immediately as 'running' so the WorkingCard renders from the
155+
// TODO: replace with rendererLogger once renderer-logger lands
156+
console.debug('[agent] tool_call_start', {
157+
generationId: event.generationId,
158+
designId,
159+
toolName,
160+
toolCallId: event.toolCallId,
161+
});
146162
// DB row rather than an in-memory shadow. Capture seq via promise so
147163
// the result handler can patch the same row even if it lands before
148164
// the append round-trip completes.
@@ -209,7 +225,13 @@ export function useAgentStream(): void {
209225

210226
const handleError = (event: AgentStreamEvent) => {
211227
const current = inFlight.current;
212-
if (current) drainPendingTools(current, 'error');
228+
// TODO: replace with rendererLogger once renderer-logger lands
229+
console.error('[agent] error', {
230+
generationId: event.generationId,
231+
designId: event.designId,
232+
message: event.message,
233+
code: event.code,
234+
});
213235
setStreamingAssistantText(null);
214236
inFlight.current = null;
215237
void appendChatMessage({

apps/desktop/src/renderer/src/store.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,8 @@ function applyGenerateError(
787787
): void {
788788
const msg = err instanceof Error ? err.message : tr('errors.unknown');
789789
if (get().activeGenerationId !== generationId) return;
790+
// TODO: replace with rendererLogger once renderer-logger lands
791+
console.error('[store] applyGenerateError', { generationId, designId: designIdAtStart, message: msg });
790792

791793
finishIfCurrent(set, generationId, () => ({
792794
isGenerating: false,
@@ -1177,6 +1179,9 @@ export const useCodesignStore = create<CodesignState>((set, get) => ({
11771179
triggerAutoRenameIfFirst(get, isFirstPrompt, request.prompt);
11781180
}
11791181

1182+
// TODO: replace with rendererLogger once renderer-logger lands
1183+
console.debug('[store] sendPrompt', { generationId, designId: designIdAtStart, promptLen: enrichedPrompt.length });
1184+
11801185
try {
11811186
await runGenerate(
11821187
get,

0 commit comments

Comments
 (0)