Skip to content
Open
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 .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.2.0"
".": "0.3.0"
}
6 changes: 3 additions & 3 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 7
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-e6a9dca1a93568e403ac72128d86f30c8c3f1336d4b67017d7e61b1836f10f47.yml
openapi_spec_hash: ef01e0649bb0e283df0aa81c369649df
config_hash: 88e87ba7021be93d267ecfc8f5e6b891
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-4fb17cafc413ae3d575e3268602b01d2d0e9ebeb734a41b6086b3353ff0d2523.yml
openapi_spec_hash: 8d48d8564849246f6f14d900c6c5f60c
config_hash: 5c69fb596588b8ace08203858518c149
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Changelog

## 0.3.0 (2025-12-20)

Full Changelog: [v0.2.0...v0.3.0](https://github.com/browserbase/stagehand-kotlin/compare/v0.2.0...v0.3.0)

### Features

* **api:** manual updates ([e4a62fb](https://github.com/browserbase/stagehand-kotlin/commit/e4a62fb0ab12733c844757cbe8ab9894f6c8c8a1))
* **api:** manual updates ([9480988](https://github.com/browserbase/stagehand-kotlin/commit/9480988b36974e37584a16a2173af3072840d4b8))
* **api:** manual updates ([d0d4e1b](https://github.com/browserbase/stagehand-kotlin/commit/d0d4e1b22d4fa7a4ea49333800a4c5137659f5e6))
* **api:** manual updates ([e631708](https://github.com/browserbase/stagehand-kotlin/commit/e63170897295bc07dc47d493573c1290eb26e5f1))
* **api:** manual updates ([1697e14](https://github.com/browserbase/stagehand-kotlin/commit/1697e144099d3d39c43d1b2ad6ab830e9c4267da))


### Chores

* **internal:** codegen related update ([c6b00cb](https://github.com/browserbase/stagehand-kotlin/commit/c6b00cb1704aee4abf1aade497aed8270e250edc))
* **internal:** codegen related update ([3b4a6f0](https://github.com/browserbase/stagehand-kotlin/commit/3b4a6f0749d57d3a817722988351a2899dca5199))
* **internal:** codegen related update ([5531737](https://github.com/browserbase/stagehand-kotlin/commit/55317375b13e137f0ae5ebd5fcc05f1393bdf633))
* **internal:** codegen related update ([842e54f](https://github.com/browserbase/stagehand-kotlin/commit/842e54f32d89d64633746138a28a1947c2f7aaf4))


### Documentation

* add more examples ([16aa06e](https://github.com/browserbase/stagehand-kotlin/commit/16aa06e0a826df71165130395e41f6fad0a4f708))

## 0.2.0 (2025-12-17)

Full Changelog: [v0.1.0...v0.2.0](https://github.com/browserbase/stagehand-kotlin/compare/v0.1.0...v0.2.0)
Expand Down
27 changes: 22 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

<!-- x-release-please-start-version -->

[![Maven Central](https://img.shields.io/maven-central/v/com.browserbase.api/stagehand-kotlin)](https://central.sonatype.com/artifact/com.browserbase.api/stagehand-kotlin/0.2.0)
[![javadoc](https://javadoc.io/badge2/com.browserbase.api/stagehand-kotlin/0.2.0/javadoc.svg)](https://javadoc.io/doc/com.browserbase.api/stagehand-kotlin/0.2.0)
[![Maven Central](https://img.shields.io/maven-central/v/com.browserbase.api/stagehand-kotlin)](https://central.sonatype.com/artifact/com.browserbase.api/stagehand-kotlin/0.3.0)
[![javadoc](https://javadoc.io/badge2/com.browserbase.api/stagehand-kotlin/0.3.0/javadoc.svg)](https://javadoc.io/doc/com.browserbase.api/stagehand-kotlin/0.3.0)

<!-- x-release-please-end -->

Expand All @@ -13,7 +13,7 @@ It is generated with [Stainless](https://www.stainless.com/).

<!-- x-release-please-start-version -->

The REST API documentation can be found on [docs.stagehand.dev](https://docs.stagehand.dev). KDocs are available on [javadoc.io](https://javadoc.io/doc/com.browserbase.api/stagehand-kotlin/0.2.0).
The REST API documentation can be found on [docs.stagehand.dev](https://docs.stagehand.dev). KDocs are available on [javadoc.io](https://javadoc.io/doc/com.browserbase.api/stagehand-kotlin/0.3.0).

<!-- x-release-please-end -->

Expand All @@ -24,7 +24,7 @@ The REST API documentation can be found on [docs.stagehand.dev](https://docs.sta
### Gradle

```kotlin
implementation("com.browserbase.api:stagehand-kotlin:0.2.0")
implementation("com.browserbase.api:stagehand-kotlin:0.3.0")
```

### Maven
Expand All @@ -33,7 +33,7 @@ implementation("com.browserbase.api:stagehand-kotlin:0.2.0")
<dependency>
<groupId>com.browserbase.api</groupId>
<artifactId>stagehand-kotlin</artifactId>
<version>0.2.0</version>
<version>0.3.0</version>
</dependency>
```

Expand Down Expand Up @@ -188,6 +188,21 @@ val response: SessionActResponse = client.sessions().act(params)

The asynchronous client supports the same options as the synchronous one, except most methods are [suspending](https://kotlinlang.org/docs/coroutines-guide.html).

## Streaming

The SDK defines methods that return response "chunk" streams, where each chunk can be individually processed as soon as it arrives instead of waiting on the full response. Streaming methods generally correspond to [SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) or [JSONL](https://jsonlines.org) responses.

Some of these methods may have streaming and non-streaming variants, but a streaming method will always have a `Streaming` suffix in its name, even if it doesn't have a non-streaming variant.

These streaming methods return [`StreamResponse`](stagehand-kotlin-core/src/main/kotlin/com/browserbase/api/core/http/StreamResponse.kt) for synchronous clients:

```kotlin
client.sessions().actStreaming(params).use { response ->
response.asSequence().forEach { println(it) }
println("No more chunks!")
}
```

## Raw responses

The SDK defines methods that deserialize responses into instances of Kotlin classes. However, these methods don't provide access to the response headers, status code, or the raw response body.
Expand Down Expand Up @@ -234,6 +249,8 @@ The SDK throws custom unchecked exception types:
| 5xx | [`InternalServerException`](stagehand-kotlin-core/src/main/kotlin/com/browserbase/api/errors/InternalServerException.kt) |
| others | [`UnexpectedStatusCodeException`](stagehand-kotlin-core/src/main/kotlin/com/browserbase/api/errors/UnexpectedStatusCodeException.kt) |

[`SseException`](stagehand-kotlin-core/src/main/kotlin/com/browserbase/api/errors/SseException.kt) is thrown for errors encountered during [SSE streaming](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) after a successful initial HTTP response.

- [`StagehandIoException`](stagehand-kotlin-core/src/main/kotlin/com/browserbase/api/errors/StagehandIoException.kt): I/O networking errors.

- [`StagehandRetryableException`](stagehand-kotlin-core/src/main/kotlin/com/browserbase/api/errors/StagehandRetryableException.kt): Generic error indicating a failure that could be retried by the client.
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ repositories {

allprojects {
group = "com.browserbase.api"
version = "0.2.0" // x-release-please-version
version = "0.3.0" // x-release-please-version
}

subprojects {
Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/stagehand.publish.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ configure<PublishingExtension> {

pom {
name.set("Stagehand API")
description.set("Stagehand SDK for AI browser automation [ALPHA].")
description.set("Stagehand SDK for AI browser automation [ALPHA]. This API allows clients to\nexecute browser automation tasks remotely on the Browserbase cloud.\n\nAll endpoints except /sessions/start require an active session ID. Responses are\nstreamed using Server-Sent Events (SSE) when the `x-stream-response: true`\nheader is provided.\n\nThis SDK is currently ALPHA software and is not production ready! Please try it\nand give us your feedback, stay tuned for upcoming release announcements!")
url.set("https://docs.stagehand.dev")

licenses {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// File generated from our OpenAPI spec by Stainless.

package com.browserbase.api.core.handlers

import com.browserbase.api.core.JsonMissing
import com.browserbase.api.core.http.HttpResponse
import com.browserbase.api.core.http.HttpResponse.Handler
import com.browserbase.api.core.http.SseMessage
import com.browserbase.api.core.http.StreamResponse
import com.browserbase.api.core.http.map
import com.browserbase.api.errors.SseException
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.jacksonTypeRef

internal fun sseHandler(jsonMapper: JsonMapper): Handler<StreamResponse<SseMessage>> =
streamHandler { response, lines ->
val state = SseState(jsonMapper)
var done = false
for (line in lines) {
// Stop emitting messages, but iterate through the full stream.
if (done) {
continue
}

val message = state.decode(line) ?: continue

when {
message.data.startsWith("finished") -> {
// In this case we don't break because we still want to iterate through the full
// stream.
done = true
continue
}
message.data.startsWith("error") -> {
throw SseException.builder()
.statusCode(response.statusCode())
.headers(response.headers())
.body(
try {
jsonMapper.readValue(message.data, jacksonTypeRef())
} catch (e: Exception) {
JsonMissing.of()
}
)
.build()
}
}

if (message.event == null) {
yield(message)
}
}
}

private class SseState(
val jsonMapper: JsonMapper,
var event: String? = null,
val data: MutableList<String> = mutableListOf(),
var lastId: String? = null,
var retry: Int? = null,
) {
// https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
fun decode(line: String): SseMessage? {
if (line.isEmpty()) {
return flush()
}

if (line.startsWith(':')) {
return null
}

val fieldName: String
var value: String

val colonIndex = line.indexOf(':')
if (colonIndex == -1) {
fieldName = line
value = ""
} else {
fieldName = line.substring(0, colonIndex)
value = line.substring(colonIndex + 1)
}

if (value.startsWith(' ')) {
value = value.substring(1)
}

when (fieldName) {
"event" -> event = value
"data" -> data.add(value)
"id" -> {
if (!value.contains('\u0000')) {
lastId = value
}
}
"retry" -> value.toIntOrNull()?.let { retry = it }
}

return null
}

private fun flush(): SseMessage? {
if (isEmpty()) {
return null
}

val message =
SseMessage.builder()
.jsonMapper(jsonMapper)
.event(event)
.data(data.joinToString("\n"))
.id(lastId)
.retry(retry)
.build()

// NOTE: Per the SSE spec, do not reset lastId.
event = null
data.clear()
retry = null

return message
}

private fun isEmpty(): Boolean =
event.isNullOrEmpty() && data.isEmpty() && lastId.isNullOrEmpty() && retry == null
}

internal inline fun <reified T> Handler<StreamResponse<SseMessage>>.mapJson():
Handler<StreamResponse<T>> =
object : Handler<StreamResponse<T>> {
override fun handle(response: HttpResponse): StreamResponse<T> =
this@mapJson.handle(response).map { it.json<T>() }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
@file:JvmName("StreamHandler")

package com.browserbase.api.core.handlers

import com.browserbase.api.core.http.HttpResponse
import com.browserbase.api.core.http.HttpResponse.Handler
import com.browserbase.api.core.http.PhantomReachableClosingStreamResponse
import com.browserbase.api.core.http.StreamResponse
import com.browserbase.api.errors.StagehandIoException
import java.io.IOException

internal fun <T> streamHandler(
block: suspend SequenceScope<T>.(response: HttpResponse, lines: Sequence<String>) -> Unit
): Handler<StreamResponse<T>> =
object : Handler<StreamResponse<T>> {

override fun handle(response: HttpResponse): StreamResponse<T> {
val reader = response.body().bufferedReader()
val sequence =
// Wrap in a `CloseableSequence` to avoid performing a read on the `reader`
// after it has been closed, which would throw an `IOException`.
CloseableSequence(
sequence {
reader.useLines { lines ->
block(
response,
// We wrap the `lines` instead of the top-level sequence because
// we only want to catch `IOException` from the reader; not from
// the user's own code.
IOExceptionWrappingSequence(lines),
)
}
}
.constrainOnce()
)

return PhantomReachableClosingStreamResponse(
object : StreamResponse<T> {

override fun asSequence(): Sequence<T> = sequence

override fun close() {
sequence.close()
reader.close()
response.close()
}
}
)
}
}

/** A sequence that catches, wraps, and rethrows [IOException] as [StagehandIoException]. */
private class IOExceptionWrappingSequence<T>(private val sequence: Sequence<T>) : Sequence<T> {

override fun iterator(): Iterator<T> {
val iterator = sequence.iterator()
return object : Iterator<T> {

override fun next(): T =
try {
iterator.next()
} catch (e: IOException) {
throw StagehandIoException("Stream failed", e)
}

override fun hasNext(): Boolean =
try {
iterator.hasNext()
} catch (e: IOException) {
throw StagehandIoException("Stream failed", e)
}
}
}
}

/**
* A sequence that can be closed.
*
* Once [close] is called, it will not yield more elements. It will also no longer consult the
* underlying [Iterator.hasNext] method.
*/
private class CloseableSequence<T>(private val sequence: Sequence<T>) : Sequence<T> {

private var isClosed: Boolean = false

override fun iterator(): Iterator<T> {
val iterator = sequence.iterator()
return object : Iterator<T> {

override fun next(): T = iterator.next()

override fun hasNext(): Boolean = !isClosed && iterator.hasNext()
}
}

fun close() {
isClosed = true
}
}
Loading
Loading