diff --git a/README.md b/README.md index f27266d..f8906dd 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ client.receive().takeWhile { env -> when (val p = env.payload) { is JobStatusEvent -> { println("status: ${p.phase} ${p.body}"); true } is JobProgress -> { println("progress: ${p.percent}% ${p.message}"); true } - is Metric -> { println("metric: ${p.name}=${p.value} ${p.unit ?: ""}"); true } + is Metric -> { println("metric: ${p.name}=${p.value} ${p.unit}"); true } is JobResultChunk -> { chunks.accept(p); true } is JobCompleted -> { println("result: ${p.result}"); false } is JobFailed -> { println("failed: ${p.code} ${p.message}"); false } @@ -222,7 +222,7 @@ try { client.receive().collect { env -> val m = env.payload as? Metric ?: return@collect if (m.name == StandardMetrics.COST_BUDGET_REMAINING) { - println("budget remaining: ${m.value} ${m.unit ?: ""}") + println("budget remaining: ${m.value} ${m.unit}") } } } catch (e: ARCPException.BudgetExhausted) { diff --git a/docs/guides/job-events.md b/docs/guides/job-events.md index cc44b55..c3d3a2b 100644 --- a/docs/guides/job-events.md +++ b/docs/guides/job-events.md @@ -8,7 +8,7 @@ progress, heartbeat, chunk, and status events. Sent when the agent begins executing: ```kotlin -is JobStarted -> println("Job ${msg.jobId} is now running") +is JobStarted -> println("Job ${env.jobId} is now running") ``` ## JobProgress diff --git a/lib/src/main/kotlin/dev/arcp/client/ResultChunkAssembler.kt b/lib/src/main/kotlin/dev/arcp/client/ResultChunkAssembler.kt index dbfefbb..ebc3a09 100644 --- a/lib/src/main/kotlin/dev/arcp/client/ResultChunkAssembler.kt +++ b/lib/src/main/kotlin/dev/arcp/client/ResultChunkAssembler.kt @@ -6,7 +6,12 @@ import dev.arcp.messages.ResultChunkEncoding import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi -/** Client-side accumulator for `result_chunk` streams (RFC v1.1 §8.4). */ +/** + * Client-side accumulator for `result_chunk` streams (RFC v1.1 §8.4). + * + * Not thread-safe: use one assembler per result stream and call [accept] + * from a single coroutine. + */ public class ResultChunkAssembler( private val maxAssembledSize: Long = DEFAULT_MAX_ASSEMBLED_SIZE, ) { diff --git a/lib/src/main/kotlin/dev/arcp/messages/Session.kt b/lib/src/main/kotlin/dev/arcp/messages/Session.kt index d0b12e0..b91ac99 100644 --- a/lib/src/main/kotlin/dev/arcp/messages/Session.kt +++ b/lib/src/main/kotlin/dev/arcp/messages/Session.kt @@ -25,7 +25,8 @@ public enum class HeartbeatRecovery { /** * Negotiated capability set (RFC §7). * - * Absent boolean fields default to `false` per §7. The `extensions` list + * Absent boolean fields default to `false` per §7, except `interrupt`, + * which defaults to `true` for SDK ergonomics. The `extensions` list * advertises the namespaces accepted on this session (RFC §21.2). */ @Serializable @@ -47,6 +48,7 @@ public data class Capabilities( @SerialName("model.use") val modelUse: Boolean = false, val anonymous: Boolean = false, + /** Deliberately defaults to `true` for pause-and-ask ergonomics. */ val interrupt: Boolean = true, @SerialName("heartbeat_interval_seconds") val heartbeatIntervalSeconds: Int = DEFAULT_HEARTBEAT_INTERVAL_SECONDS, diff --git a/lib/src/test/kotlin/dev/arcp/runtime/CapabilityNegotiationTest.kt b/lib/src/test/kotlin/dev/arcp/runtime/CapabilityNegotiationTest.kt new file mode 100644 index 0000000..ddea4ec --- /dev/null +++ b/lib/src/test/kotlin/dev/arcp/runtime/CapabilityNegotiationTest.kt @@ -0,0 +1,17 @@ +package dev.arcp.runtime + +import dev.arcp.messages.Capabilities +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class CapabilityNegotiationTest : + StringSpec({ + "interrupt stays enabled when both peers use defaults" { + val proposed = Capabilities() + val supported = Capabilities() + + val result = negotiate(proposed, supported) + + result.negotiated.interrupt shouldBe true + } + })