Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .fern/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"enable-wire-tests": true,
"runtime-version": true
},
"originGitCommit": "3dc7e52769409adb2a41e006ee1fc80fdf642a20",
"originGitCommit": "7d90d34144ca944bba2a570780fad2d53fdc5c4d",
"originGitCommitIsDirty": true,
"invokedBy": "manual",
"sdkVersion": "0.11.2"
Expand Down
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ Current permanently frozen files:
- `src/test/` - manually maintained tests
- `examples/` - manually maintained examples
- `.editorconfig`, `.githooks/`, `.github/`, `.gitignore` - repo and CI configuration
- `context7.json` - generated-documentation context configuration maintained outside Fern
- `target/` - build output
- `CLAUDE.md`, `AGENTS.md`, `.claude/` - agent files
- `CLAUDE.md`, `AGENTS.md`, `.claude/`, `.agents/` - agent files and agent-agnostic skills

Note: `.fernignore` also includes flat-path variants such as `src/main/java/DeepgramClient.java` as defensive entries for alternate local generation layouts. Those files do not currently exist in this repo checkout and should not be treated as current maintained source files.

Expand All @@ -49,7 +50,7 @@ How to identify:
Current temporarily frozen files:

- `src/main/java/com/deepgram/resources/agent/v1/websocket/V1WebSocketClient.java` - Agent V1 already delivers every raw control frame through `onMessage(String)`. Suppress generic errors for unknown frames only when a raw handler is registered; otherwise retain legacy `onError` delivery. Preserve the generated Think Live, dynamic-injection, and frustration handlers during reconciliation.
- `src/main/java/com/deepgram/resources/agent/v1/types/AgentV1ConversationText.java` - keeps newly added `conversationalBehavior(...)` methods on the public `_FinalStage` builder interface as default no-ops. Generated concrete builders override them normally; the defaults preserve source and binary compatibility for third-party implementations compiled before Think Live existed.
- `src/main/java/com/deepgram/resources/agent/v1/types/AgentV1ConversationText.java` restores the legacy `content(...): _FinalStage` builder stage and defaults omitted or empty `conversational_behavior` to `default`. The schema now requires the wire field, while this shim preserves source and binary compatibility for callers compiled before Think Live existed. `ConversationalBehaviorStage` remains as an intentionally retained generated type, even though the restored legacy stage no longer returns it.
- `src/main/java/com/deepgram/resources/agent/v1/types/AgentV1ThinkLiveUpdated.java` - fields-less event type with a manual `hashCode()` patch. Fern generates `equals()` but no matching `hashCode()`, violating the Object contract; retain the patch, freeze entry, and `RegenTypesTest.FieldsLessMessageContract` assertion until Fern fixes the pattern.
- `src/main/java/com/deepgram/core/ClientOptions.java` - preserves release-please version markers, correct SDK header constants, and the accurate `close()` lifecycle Javadoc that Fern currently overwrites; use the standard `.bak` swap/restore workflow during regen review. Since generator 4.18.0 Fern emits a `getSdkVersion()` helper reading `Package.getImplementationVersion()` instead of a literal. That *does* resolve in the published artifact (CI publishes via `./mvnw deploy -P release`, and `pom.xml`'s maven-jar-plugin sets `addDefaultImplementationEntries=true`, so the JAR manifest carries `Implementation-Version`), but it resolves to `null` under Gradle and in tests, where it silently falls back to a hardcoded literal the generator does not keep current. We keep the explicit literals because they are correct in every context and because `.github/release-please-config.json` already lists this file in `extra-files`, so release-please bumps it alongside `pom.xml`, `build.gradle`, and `.fern/metadata.json`. Fern also emits `User-Agent` with a `com.deepgram.` prefix while leaving `X-Fern-SDK-Name` on the `com.deepgram:` Maven-coordinate form; we keep both on the colon form. Fern 4.22.1 tracks and closes child WebSockets before shutting down SDK-owned OkHttp resources; the permanent custom client wrappers inherit it, so do not add wrapper-local lifecycle code and retain Fern's lifecycle implementation during reconciliation.
- `src/main/java/com/deepgram/core/ReconnectingWebSocketListener.java` - carries bug fixes for `maxRetries(0)` semantics ("connect once, don't retry") and a configurable `connectionTimeoutMs` field (was hardcoded 4000ms), plus an `applyOptionsOverride(...)` hook used by `TransportWebSocketFactory` to apply per-transport reconnect policy; pull this back out once the fixes are upstreamed into the Fern generator. Use the standard `.bak` swap/restore workflow during regen review.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public final class AgentV1ConversationText {

private final String content;

private final Optional<AgentV1ConversationTextConversationalBehavior> conversationalBehavior;
private final AgentV1ConversationTextConversationalBehavior conversationalBehavior;

private final Optional<List<String>> languagesHinted;

Expand All @@ -37,7 +37,7 @@ public final class AgentV1ConversationText {
private AgentV1ConversationText(
AgentV1ConversationTextRole role,
String content,
Optional<AgentV1ConversationTextConversationalBehavior> conversationalBehavior,
AgentV1ConversationTextConversationalBehavior conversationalBehavior,
Optional<List<String>> languagesHinted,
Optional<List<String>> languages,
Map<String, Object> additionalProperties) {
Expand Down Expand Up @@ -78,7 +78,7 @@ public String getContent() {
*/
@JsonProperty("conversational_behavior")
public Optional<AgentV1ConversationTextConversationalBehavior> getConversationalBehavior() {
return conversationalBehavior;
return Optional.of(conversationalBehavior);
}

/**
Expand Down Expand Up @@ -146,22 +146,28 @@ public interface ContentStage {
_FinalStage content(@NotNull String content);
}

public interface ConversationalBehaviorStage {
/**
* <p>Annotates what kind of utterance the message represents: <code>default</code> (a standard turn), <code>backchannel</code> (a short acknowledgement while the user held the floor), <code>presence_check</code> (a prompt after extended silence), or <code>filler</code> (a line covering a wait on a function call or a dynamic InjectAgentMessage). Always sent; it is <code>default</code> for every message while Think Live is off</p>
*/
_FinalStage conversationalBehavior(
@NotNull AgentV1ConversationTextConversationalBehavior conversationalBehavior);
}

public interface _FinalStage {
AgentV1ConversationText build();

_FinalStage additionalProperty(String key, Object value);

_FinalStage additionalProperties(Map<String, Object> additionalProperties);

/**
* <p>Annotates what kind of utterance the message represents: <code>default</code> (a standard turn), <code>backchannel</code> (a short acknowledgement while the user held the floor), <code>presence_check</code> (a prompt after extended silence), or <code>filler</code> (a line covering a wait on a function call or a dynamic InjectAgentMessage). Always sent; it is <code>default</code> for every message while Think Live is off</p>
*/
default _FinalStage conversationalBehavior(
Optional<AgentV1ConversationTextConversationalBehavior> conversationalBehavior) {
@NotNull AgentV1ConversationTextConversationalBehavior conversationalBehavior) {
return this;
}

default _FinalStage conversationalBehavior(AgentV1ConversationTextConversationalBehavior conversationalBehavior) {
default _FinalStage conversationalBehavior(
Optional<AgentV1ConversationTextConversationalBehavior> conversationalBehavior) {
return this;
}

Expand All @@ -181,17 +187,18 @@ default _FinalStage conversationalBehavior(AgentV1ConversationTextConversational
}

@JsonIgnoreProperties(ignoreUnknown = true)
public static final class Builder implements RoleStage, ContentStage, _FinalStage {
public static final class Builder implements RoleStage, ContentStage, ConversationalBehaviorStage, _FinalStage {
private AgentV1ConversationTextRole role;

private String content;

private AgentV1ConversationTextConversationalBehavior conversationalBehavior =
AgentV1ConversationTextConversationalBehavior.DEFAULT;

private Optional<List<String>> languages = Optional.empty();

private Optional<List<String>> languagesHinted = Optional.empty();

private Optional<AgentV1ConversationTextConversationalBehavior> conversationalBehavior = Optional.empty();

@JsonAnySetter
private Map<String, Object> additionalProperties = new HashMap<>();

Expand Down Expand Up @@ -229,6 +236,26 @@ public _FinalStage content(@NotNull String content) {
return this;
}

/**
* <p>Annotates what kind of utterance the message represents: <code>default</code> (a standard turn), <code>backchannel</code> (a short acknowledgement while the user held the floor), <code>presence_check</code> (a prompt after extended silence), or <code>filler</code> (a line covering a wait on a function call or a dynamic InjectAgentMessage). Always sent; it is <code>default</code> for every message while Think Live is off</p>
* @return Reference to {@code this} so that method calls can be chained together.
*/
@java.lang.Override
@JsonSetter(value = "conversational_behavior", nulls = Nulls.SKIP)
public _FinalStage conversationalBehavior(
@NotNull AgentV1ConversationTextConversationalBehavior conversationalBehavior) {
this.conversationalBehavior =
Objects.requireNonNull(conversationalBehavior, "conversationalBehavior must not be null");
return this;
}

public _FinalStage conversationalBehavior(
Optional<AgentV1ConversationTextConversationalBehavior> conversationalBehavior) {
this.conversationalBehavior = conversationalBehavior.orElse(
AgentV1ConversationTextConversationalBehavior.DEFAULT);
return this;
}

/**
* <p>Languages detected in the user's speech, sorted by word count (descending). Only present on user-role messages when the listen model is flux-general-multi.</p>
* @return Reference to {@code this} so that method calls can be chained together.
Expand Down Expand Up @@ -269,28 +296,6 @@ public _FinalStage languagesHinted(Optional<List<String>> languagesHinted) {
return this;
}

/**
* <p>Annotates what kind of utterance the message represents: <code>default</code> (a standard turn), <code>backchannel</code> (a short acknowledgement while the user held the floor), <code>presence_check</code> (a prompt after extended silence), or <code>filler</code> (a line covering a wait on a function call or a dynamic InjectAgentMessage). Always sent; it is <code>default</code> for every message while Think Live is off</p>
* @return Reference to {@code this} so that method calls can be chained together.
*/
@java.lang.Override
public _FinalStage conversationalBehavior(
AgentV1ConversationTextConversationalBehavior conversationalBehavior) {
this.conversationalBehavior = Optional.ofNullable(conversationalBehavior);
return this;
}

/**
* <p>Annotates what kind of utterance the message represents: <code>default</code> (a standard turn), <code>backchannel</code> (a short acknowledgement while the user held the floor), <code>presence_check</code> (a prompt after extended silence), or <code>filler</code> (a line covering a wait on a function call or a dynamic InjectAgentMessage). Always sent; it is <code>default</code> for every message while Think Live is off</p>
*/
@java.lang.Override
@JsonSetter(value = "conversational_behavior", nulls = Nulls.SKIP)
public _FinalStage conversationalBehavior(
Optional<AgentV1ConversationTextConversationalBehavior> conversationalBehavior) {
this.conversationalBehavior = conversationalBehavior;
return this;
}

@java.lang.Override
public AgentV1ConversationText build() {
return new AgentV1ConversationText(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,19 @@ private void handleIncomingMessage(String json) {
if (node == null || node.isNull()) {
throw new IllegalArgumentException("Received null or invalid JSON message");
}
if (node.has("role") && node.has("content") && "ConversationText".equals(node.path("type").asText())) {
AgentV1ConversationText conversationTextHandlerEvent = null;
try {
conversationTextHandlerEvent = objectMapper.treeToValue(node, AgentV1ConversationText.class);
} catch (Exception e) {
}
if (conversationTextHandlerEvent != null) {
if (conversationTextHandler != null) {
conversationTextHandler.accept(conversationTextHandlerEvent);
}
return;
}
}
if (node.has("total_latency")
&& node.has("tts_latency")
&& node.has("ttt_latency")
Expand Down Expand Up @@ -712,21 +725,6 @@ private void handleIncomingMessage(String json) {
return;
}
}
if (node.has("role")
&& node.has("content")
&& "ConversationText".equals(node.path("type").asText())) {
AgentV1ConversationText conversationTextHandlerEvent = null;
try {
conversationTextHandlerEvent = objectMapper.treeToValue(node, AgentV1ConversationText.class);
} catch (Exception e) {
}
if (conversationTextHandlerEvent != null) {
if (conversationTextHandler != null) {
conversationTextHandler.accept(conversationTextHandlerEvent);
}
return;
}
}
if (node.has("description")
&& node.has("code")
&& "Error".equals(node.path("type").asText())) {
Expand Down
65 changes: 65 additions & 0 deletions src/test/java/com/deepgram/AgentV1ThinkLiveWireTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,71 @@ public void onClosing(WebSocket webSocket, int code, String reason) {
}
}

@Test
void dispatchesConversationTextWithoutBehaviorAsDefault() throws Exception {
CountDownLatch received = new CountDownLatch(1);
AtomicReference<AgentV1ConversationText> conversationText = new AtomicReference<>();
server.enqueue(new MockResponse().withWebSocketUpgrade(new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, okhttp3.Response response) {
webSocket.send("{\"type\":\"ConversationText\",\"role\":\"assistant\",\"content\":\"Hello\"}");
}

@Override
public void onClosing(WebSocket webSocket, int code, String reason) {
webSocket.close(code, reason);
}
}));

V1WebSocketClient ws = client.agent().v1().v1WebSocket();
ws.onConversationText(event -> {
conversationText.set(event);
received.countDown();
});
try {
ws.connect().get(5, TimeUnit.SECONDS);

assertThat(received.await(5, TimeUnit.SECONDS)).isTrue();
assertThat(conversationText.get().getConversationalBehavior())
.contains(AgentV1ConversationTextConversationalBehavior.DEFAULT);
} finally {
ws.disconnect();
}
}

@Test
void dispatchesConversationTextWithNullBehaviorAsDefault() throws Exception {
CountDownLatch received = new CountDownLatch(1);
AtomicReference<AgentV1ConversationText> conversationText = new AtomicReference<>();
server.enqueue(new MockResponse().withWebSocketUpgrade(new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, okhttp3.Response response) {
webSocket.send("{\"type\":\"ConversationText\",\"role\":\"assistant\","
+ "\"content\":\"Hello\",\"conversational_behavior\":null}");
}

@Override
public void onClosing(WebSocket webSocket, int code, String reason) {
webSocket.close(code, reason);
}
}));

V1WebSocketClient ws = client.agent().v1().v1WebSocket();
ws.onConversationText(event -> {
conversationText.set(event);
received.countDown();
});
try {
ws.connect().get(5, TimeUnit.SECONDS);

assertThat(received.await(5, TimeUnit.SECONDS)).isTrue();
assertThat(conversationText.get().getConversationalBehavior())
.contains(AgentV1ConversationTextConversationalBehavior.DEFAULT);
} finally {
ws.disconnect();
}
}

@Test
void ignoresUnknownFramesAfterDeliveringThemRaw() throws Exception {
String unknownFrame = "{\"type\":\"FutureAgentControl\",\"feature\":true}";
Expand Down
Loading
Loading