Skip to content

Commit 2b6c25b

Browse files
committed
fix(agent): patch History event handling after regen
- dispatch Agent History websocket events on wire type "History" - deserialize History variants by payload shape instead of message name - add regression tests for conversation-text and function-call History events - freeze and document the Agent History patch for future regens
1 parent aaf8a11 commit 2b6c25b

7 files changed

Lines changed: 131 additions & 11 deletions

File tree

.claude/skills/prepare-regen.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Read AGENTS.md for full context on the regeneration workflow and freeze classifi
1212
4. Read `.fernignore` and classify each entry using the rules in AGENTS.md:
1313
- **Permanently frozen**: entirely hand-written, no Fern equivalent. NEVER touch these.
1414
- **Temporarily frozen**: Fern-generated with manual patches. These get swapped.
15+
- Current Java temporary freezes include `ClientOptions.java` plus the Agent History patch files `V1WebSocketClient.java` and `AgentV1History.java`.
1516
5. For each **temporarily frozen** file only:
1617
- Copy the file to `<filename>.bak` alongside the original.
1718
- In `.fernignore`, replace the original path with the `.bak` path (protects our patch, lets Fern overwrite the original).

.claude/skills/review-regen.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Read AGENTS.md for full context on the regeneration workflow and freeze classifi
1414
- Patches still needed (must be re-applied)
1515
- New changes from the generator worth noting
1616
5. Wait for user direction on which patches to re-apply.
17-
6. Re-apply confirmed patches to the generated files. If `src/main/java/com/deepgram/core/ClientOptions.java` was regenerated, run `python3 .claude/scripts/fix_clientoptions.py` to restore the Deepgram SDK header constants and `// x-release-please-version` markers.
17+
6. Re-apply confirmed patches to the generated files. If `src/main/java/com/deepgram/core/ClientOptions.java` was regenerated, run `python3 .claude/scripts/fix_clientoptions.py` to restore the Deepgram SDK header constants and `// x-release-please-version` markers. If Fern still generates Agent History incorrectly, re-apply the manual patches in `src/main/java/com/deepgram/resources/agent/v1/websocket/V1WebSocketClient.java` and `src/main/java/com/deepgram/resources/agent/v1/types/AgentV1History.java` so websocket dispatch uses wire type `"History"` and the history union selects variants by payload shape.
1818
7. In `.fernignore`, replace each `.bak` path back to the original path for files that still need patches.
1919
8. Remove `.fernignore` entries entirely for files where patches are no longer needed.
2020
9. Delete all `.bak` files.

.fernignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ src/main/java/com/deepgram/AsyncDeepgramClientBuilder.java
1616
# Fern regen overwrites these with incorrect SDK names and strips the markers.
1717
src/main/java/com/deepgram/core/ClientOptions.java
1818

19+
# Agent History websocket patch
20+
# Fern currently dispatches on the message name instead of wire type "History",
21+
# and the generated union needs explicit variant selection for content vs function_calls.
22+
src/main/java/com/deepgram/resources/agent/v1/websocket/V1WebSocketClient.java
23+
src/main/java/com/deepgram/resources/agent/v1/types/AgentV1History.java
24+
1925
# Transport abstraction (pluggable transport for SageMaker, etc.)
2026
src/main/java/com/deepgram/core/transport/
2127

AGENTS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ How to identify:
4848
Current temporarily frozen files:
4949

5050
- `src/main/java/com/deepgram/core/ClientOptions.java` - preserves release-please version markers and correct SDK header constants that Fern currently overwrites; re-apply with `python3 .claude/scripts/fix_clientoptions.py` during review regen
51+
- `src/main/java/com/deepgram/resources/agent/v1/websocket/V1WebSocketClient.java` - patches Fern's generated Agent History websocket dispatch to use wire type `"History"` instead of message name `"AgentV1History"`
52+
- `src/main/java/com/deepgram/resources/agent/v1/types/AgentV1History.java` - patches Fern's generated Agent History union deserializer to choose the correct variant by payload shape (`role`/`content` vs `function_calls`)
5153

5254
### Prepare repo for regeneration
5355

@@ -66,7 +68,7 @@ Current temporarily frozen files:
6668
The `.bak` files are our manually patched versions protected by `.fernignore`. The original paths now contain the freshly generated versions. By comparing the two, we can see what the generator now produces versus what we had patched.
6769

6870
1. Diff each `.bak` file against the new generated version to understand what changed and whether our patches are still needed.
69-
2. Re-apply any patches that are still necessary to the newly generated files. For `src/main/java/com/deepgram/core/ClientOptions.java`, run `python3 .claude/scripts/fix_clientoptions.py` to restore the Deepgram SDK header constants and `// x-release-please-version` markers.
71+
2. Re-apply any patches that are still necessary to the newly generated files. For `src/main/java/com/deepgram/core/ClientOptions.java`, run `python3 .claude/scripts/fix_clientoptions.py` to restore the Deepgram SDK header constants and `// x-release-please-version` markers. If Agent History is still generated incorrectly, re-apply the `V1WebSocketClient.java` and `AgentV1History.java` patches and keep those files frozen.
7072
3. In `.fernignore`, replace each `.bak` path back to the original path for files that still need patches.
7173
4. Remove `.fernignore` entries entirely for any files where the generator now produces correct output.
7274
5. Delete all `.bak` files once review is complete.

src/main/java/com/deepgram/resources/agent/v1/types/AgentV1History.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.fasterxml.jackson.annotation.JsonValue;
88
import com.fasterxml.jackson.core.JsonParseException;
99
import com.fasterxml.jackson.core.JsonParser;
10+
import com.fasterxml.jackson.databind.JsonNode;
1011
import com.fasterxml.jackson.databind.DeserializationContext;
1112
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
1213
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
@@ -80,16 +81,19 @@ static final class Deserializer extends StdDeserializer<AgentV1History> {
8081

8182
@java.lang.Override
8283
public AgentV1History deserialize(JsonParser p, DeserializationContext context) throws IOException {
83-
Object value = p.readValueAs(Object.class);
84-
try {
85-
return of(ObjectMappers.JSON_MAPPER.convertValue(value, AgentV1HistoryContent.class));
86-
} catch (RuntimeException e) {
84+
JsonNode value = p.readValueAsTree();
85+
86+
if (value.hasNonNull("function_calls")) {
87+
return of(ObjectMappers.JSON_MAPPER.treeToValue(value, AgentV1HistoryFunctionCalls.class));
8788
}
88-
try {
89-
return of(ObjectMappers.JSON_MAPPER.convertValue(value, AgentV1HistoryFunctionCalls.class));
90-
} catch (RuntimeException e) {
89+
90+
if (value.hasNonNull("role") || value.hasNonNull("content")) {
91+
return of(ObjectMappers.JSON_MAPPER.treeToValue(value, AgentV1HistoryContent.class));
9192
}
92-
throw new JsonParseException(p, "Failed to deserialize");
93+
94+
throw new JsonParseException(
95+
p,
96+
"Failed to deserialize AgentV1History: expected either role/content or function_calls payload");
9397
}
9498
}
9599
}

src/main/java/com/deepgram/resources/agent/v1/websocket/V1WebSocketClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,7 @@ private void handleIncomingMessage(String json) {
664664
}
665665
}
666666
break;
667-
case "AgentV1History":
667+
case "History":
668668
if (agentV1HistoryHandler != null) {
669669
AgentV1History event = objectMapper.treeToValue(node, AgentV1History.class);
670670
if (event != null) {
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package com.deepgram.resources.agent.v1.websocket;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import com.deepgram.core.ClientOptions;
6+
import com.deepgram.core.Environment;
7+
import com.deepgram.resources.agent.v1.types.AgentV1History;
8+
import com.deepgram.resources.agent.v1.types.AgentV1HistoryContent;
9+
import com.deepgram.resources.agent.v1.types.AgentV1HistoryContentRole;
10+
import com.deepgram.resources.agent.v1.types.AgentV1HistoryFunctionCalls;
11+
import java.lang.reflect.Method;
12+
import java.util.concurrent.atomic.AtomicReference;
13+
import org.junit.jupiter.api.DisplayName;
14+
import org.junit.jupiter.api.Test;
15+
16+
class V1WebSocketClientTest {
17+
18+
@Test
19+
@DisplayName("dispatches History conversation text messages to the typed history handler")
20+
void dispatchesHistoryConversationMessages() throws Exception {
21+
V1WebSocketClient client = createClient();
22+
AtomicReference<AgentV1History> history = new AtomicReference<>();
23+
AtomicReference<Exception> error = new AtomicReference<>();
24+
client.onAgentV1History(history::set);
25+
client.onError(error::set);
26+
27+
invokeHandleIncomingMessage(client, "{\"type\":\"History\",\"role\":\"user\",\"content\":\"hello\"}");
28+
29+
assertThat(error.get()).isNull();
30+
assertThat(history.get()).isNotNull();
31+
32+
String variant = history.get().visit(new AgentV1History.Visitor<String>() {
33+
@Override
34+
public String visit(AgentV1HistoryContent value) {
35+
assertThat(value.getType()).isEqualTo("History");
36+
assertThat(value.getRole()).isEqualTo(AgentV1HistoryContentRole.USER);
37+
assertThat(value.getContent()).isEqualTo("hello");
38+
return "content";
39+
}
40+
41+
@Override
42+
public String visit(AgentV1HistoryFunctionCalls value) {
43+
return "function_calls";
44+
}
45+
});
46+
47+
assertThat(variant).isEqualTo("content");
48+
}
49+
50+
@Test
51+
@DisplayName("dispatches History function call messages to the typed history handler")
52+
void dispatchesHistoryFunctionCallMessages() throws Exception {
53+
V1WebSocketClient client = createClient();
54+
AtomicReference<AgentV1History> history = new AtomicReference<>();
55+
AtomicReference<Exception> error = new AtomicReference<>();
56+
client.onAgentV1History(history::set);
57+
client.onError(error::set);
58+
59+
invokeHandleIncomingMessage(
60+
client,
61+
"{" +
62+
"\"type\":\"History\"," +
63+
"\"function_calls\":[{" +
64+
"\"id\":\"fc_123\"," +
65+
"\"name\":\"lookup_weather\"," +
66+
"\"client_side\":true," +
67+
"\"arguments\":\"{\\\"city\\\":\\\"London\\\"}\"," +
68+
"\"response\":\"sunny\"" +
69+
"}]}" );
70+
71+
assertThat(error.get()).isNull();
72+
assertThat(history.get()).isNotNull();
73+
74+
String variant = history.get().visit(new AgentV1History.Visitor<String>() {
75+
@Override
76+
public String visit(AgentV1HistoryContent value) {
77+
return "content";
78+
}
79+
80+
@Override
81+
public String visit(AgentV1HistoryFunctionCalls value) {
82+
assertThat(value.getType()).isEqualTo("History");
83+
assertThat(value.getFunctionCalls()).hasSize(1);
84+
assertThat(value.getFunctionCalls().get(0).getId()).isEqualTo("fc_123");
85+
assertThat(value.getFunctionCalls().get(0).getName()).isEqualTo("lookup_weather");
86+
assertThat(value.getFunctionCalls().get(0).getClientSide()).isTrue();
87+
assertThat(value.getFunctionCalls().get(0).getArguments()).isEqualTo("{\"city\":\"London\"}");
88+
assertThat(value.getFunctionCalls().get(0).getResponse()).isEqualTo("sunny");
89+
return "function_calls";
90+
}
91+
});
92+
93+
assertThat(variant).isEqualTo("function_calls");
94+
}
95+
96+
private static V1WebSocketClient createClient() {
97+
ClientOptions clientOptions =
98+
ClientOptions.builder().environment(Environment.PRODUCTION).build();
99+
return new V1WebSocketClient(clientOptions);
100+
}
101+
102+
private static void invokeHandleIncomingMessage(V1WebSocketClient client, String json) throws Exception {
103+
Method handleIncomingMessage = V1WebSocketClient.class.getDeclaredMethod("handleIncomingMessage", String.class);
104+
handleIncomingMessage.setAccessible(true);
105+
handleIncomingMessage.invoke(client, json);
106+
}
107+
}

0 commit comments

Comments
 (0)