M1+M2: gateway core loop + agent transparency

M1 (gateway core loop / text round-trip):
- WS server (ws_server.py): bind, hello auth (constant-time), hello.ack, heartbeat, connection registry
- pairing.py: token generation, pairing store, QR payload
- adapter.py: send() -> message frame; inbound message.send -> MessageEvent -> handle_message
- app: Connect screen, GatewayClient (connect + reconnect), ChatScreen send/render, SecureStore (Android/Desktop)
- tests/ws_probe.py: probe harness driving a real turn

M2 (streaming + reasoning + tools + commentary):
- protocol.py: M2 frame types (message.start/update/stop, tool.start/progress/end, commentary)
- adapter.py: per-chat turn-state machine; classify outbound into frames; _split_reasoning; tool-line parsing
- reasoning in streaming: capture via on_stream_delta hook (kind=reasoning, gated by plugins.stream_reasoning_deltas) with a FIFO barrier, attach to message.stop
- app: live streaming bubble, ReasoningBlock (collapse + copy), ToolCard (Everything/Truncated/Nothing), dimmed commentary, typing
- docs/14-milestones.md: M1/M2 marked done; reasoning note corrected
This commit is contained in:
ARIA committed 2026-08-19 13:56:19 +02:00
1 parent 59acf66c89
commit 218c50d688
21 files changed
+3437 -129

No files matched your search

@@ -0,0 +1,122 @@
package iris.state
import iris.data.ChatStore
import iris.data.SecureStore
import iris.net.GatewayClient
import iris.protocol.TYPE_COMMENTARY
import iris.protocol.TYPE_MESSAGE
import iris.protocol.TYPE_MESSAGE_START
import iris.protocol.TYPE_MESSAGE_STOP
import iris.protocol.TYPE_MESSAGE_UPDATE
import iris.protocol.TYPE_TOOL_END
import iris.protocol.TYPE_TOOL_PROGRESS
import iris.protocol.TYPE_TOOL_START
import iris.protocol.TYPE_TYPING
import iris.protocol.TypingPayload
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
/**
* App-level controller (M2): owns the GatewayClient + ChatStore, routes
* server frames (message / streaming / tool / commentary / typing) into
* state, and exposes the actions the UI needs.
*/
class IrisController(
private val store: SecureStore,
) {
private val job = SupervisorJob()
private val scope = CoroutineScope(job + Dispatchers.Default)
val client = GatewayClient(scope, store)
val chat = ChatStore()
private val _typing = MutableStateFlow(false)
val typing: StateFlow<Boolean> = _typing.asStateFlow()
/** Tool-card verbosity (M2). NOTHING suppresses tool cards entirely. */
private val _toolDetail = MutableStateFlow(ToolDetail.TRUNCATED)
val toolDetail: StateFlow<ToolDetail> = _toolDetail.asStateFlow()
fun cycleToolDetail() {
_toolDetail.value = when (_toolDetail.value) {
ToolDetail.EVERYTHING -> ToolDetail.TRUNCATED
ToolDetail.TRUNCATED -> ToolDetail.NOTHING
ToolDetail.NOTHING -> ToolDetail.EVERYTHING
}
}
/** Home channel id (from hello.ack; default until then). */
private val _homeChannel = MutableStateFlow("android:default")
val homeChannel: StateFlow<String> = _homeChannel.asStateFlow()
init {
scope.launch {
client.events.collect { frame ->
when (frame.type) {
TYPE_MESSAGE,
TYPE_MESSAGE_START,
TYPE_MESSAGE_UPDATE,
TYPE_MESSAGE_STOP,
TYPE_TOOL_START,
TYPE_TOOL_PROGRESS,
TYPE_TOOL_END,
TYPE_COMMENTARY -> chat.onFrame(frame)
TYPE_TYPING -> {
frame.payloadAs<TypingPayload>()?.let { _typing.value = it.on }
}
else -> Unit
}
}
}
scope.launch {
client.state.collect { s ->
if (s is GatewayClient.State.Connected) {
val home = s.channels.firstOrNull { it.isDefault }?.chatId
if (home != null) _homeChannel.value = home
}
}
}
client.startHeartbeat()
client.start()
}
/** Optimistic send: show immediately, then hand to the gateway. */
fun send(text: String) {
val trimmed = text.trim()
if (trimmed.isEmpty()) return
chat.addPending(trimmed)
client.sendMessage(_homeChannel.value, trimmed)
}
/** Connect-screen action: real hello test, then save + (re)connect. */
suspend fun connect(url: String, token: String): Result<Unit> {
val test = client.testHello(url, token)
if (test.isFailure) return test
store.savePairing(url, token)
client.restart()
return Result.success(Unit)
}
fun forget() {
store.clear()
chat.clear()
client.restart()
}
fun dispose() {
client.stop()
job.cancel()
}
}
/** How much tool detail to show (Settings → "Tool detail"). */
enum class ToolDetail {
EVERYTHING, // name + full args (collapsible) + output preview
TRUNCATED, // one-liner "emoji name: preview", collapsible (default)
NOTHING, // suppress tool cards entirely
}