Fix agent event ordering: update state before emitting events

Previously, Agent.emit() was called before state was updated (e.g., appendMessage).
This meant event handlers saw stale state - when message_end fired,
agent.state.messages didn't include the message yet.

Now state is updated first, then events are emitted, so handlers see
consistent state that matches the event.
This commit is contained in:
Mario Zechner 2025-12-09 14:33:39 +01:00
parent 1194fb8afa
commit 2b0aa5ed8e
4 changed files with 321 additions and 659 deletions

951
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -255,25 +255,20 @@ export class Agent {
cfg, cfg,
this.abortController.signal, this.abortController.signal,
)) { )) {
// Pass through all events directly // Update internal state BEFORE emitting events
this.emit(ev as AgentEvent); // so handlers see consistent state
// Update internal state as needed
switch (ev.type) { switch (ev.type) {
case "message_start": { case "message_start": {
// Track streaming message
partial = ev.message; partial = ev.message;
this._state.streamMessage = ev.message; this._state.streamMessage = ev.message;
break; break;
} }
case "message_update": { case "message_update": {
// Update streaming message
partial = ev.message; partial = ev.message;
this._state.streamMessage = ev.message; this._state.streamMessage = ev.message;
break; break;
} }
case "message_end": { case "message_end": {
// Add completed message to state
partial = null; partial = null;
this._state.streamMessage = null; this._state.streamMessage = null;
this.appendMessage(ev.message as AppMessage); this.appendMessage(ev.message as AppMessage);
@ -293,7 +288,6 @@ export class Agent {
break; break;
} }
case "turn_end": { case "turn_end": {
// Capture error from turn_end event
if (ev.message.role === "assistant" && ev.message.errorMessage) { if (ev.message.role === "assistant" && ev.message.errorMessage) {
this._state.error = ev.message.errorMessage; this._state.error = ev.message.errorMessage;
} }
@ -304,6 +298,9 @@ export class Agent {
break; break;
} }
} }
// Emit after state is updated
this.emit(ev as AgentEvent);
} }
// Handle any remaining partial message // Handle any remaining partial message

View file

@ -172,7 +172,7 @@ describe.skipIf(!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_OAUTH_T
// Find assistant's response // Find assistant's response
const messageEndEvents = events.filter((e) => e.type === "message_end") as AgentEvent[]; const messageEndEvents = events.filter((e) => e.type === "message_end") as AgentEvent[];
const assistantMessage = messageEndEvents.find( const assistantMessage = messageEndEvents.find(
(e) => e.type === "message_end" && (e as any).message?.role === "assistant", (e) => e.type === "message_end" && e.message?.role === "assistant",
) as any; ) as any;
expect(assistantMessage).toBeDefined(); expect(assistantMessage).toBeDefined();

View file

@ -1,22 +1,8 @@
{ {
"folders": [ "folders": [
{
"name": "sitegeist",
"path": "../sitegeist"
},
{ {
"name": "pi-mono", "name": "pi-mono",
"path": "." "path": "."
},
{
"name": "mini-lit",
"path": "../mini-lit"
},
{
"path": "../../agent-tools/browser-tools"
},
{
"path": "../pi-terminal-bench"
} }
], ],
"settings": {} "settings": {}