feat(tap): additive v1 protocol extensions (spec §3) — speech_started/stopped, function_call, function_call_output, tools.update
Five new event types in slice-2's v1 protocol. Forwards-compatible per slice-2 §3.4: FrameKind's #[serde(other)] Unknown absorbs the new types in old brains (they log + count + drop). No wire-format break, no version bump. New kid on the dispatch: tools.update — brain declares its catalog on hello so the core's tool registry can validate function_call events by name (Task 6). The 'function_call'/'function_call_output' pair is the FOB-boundary dispatch contract the brain's translator (Task 4) wires to OpenAI Realtime's response.function_call_arguments.done / conversation.item.create. TDD: 6 tests fail-red on missing impl, pass-green after this task. Slice-2's existing protocol tests stay green — the additions are purely additive.
This commit is contained in:
@@ -41,8 +41,12 @@ pub use protocol::{
|
|||||||
AudioPayload, DecodedFrame, DecodedPayload, Envelope, ErrorPayload, FrameKind, HelloPayload,
|
AudioPayload, DecodedFrame, DecodedPayload, Envelope, ErrorPayload, FrameKind, HelloPayload,
|
||||||
PROTOCOL_VERSION, Payload, ReasonPayload, SAMPLES_PER_FRAME as WIRE_SAMPLES_PER_FRAME,
|
PROTOCOL_VERSION, Payload, ReasonPayload, SAMPLES_PER_FRAME as WIRE_SAMPLES_PER_FRAME,
|
||||||
SessionEndPayload, TapProtoError, decode_envelope, decode_pcm, encode_audio_in,
|
SessionEndPayload, TapProtoError, decode_envelope, decode_pcm, encode_audio_in,
|
||||||
encode_audio_out, encode_bye, encode_error, encode_hello, encode_pcm, encode_session_end,
|
encode_audio_out, encode_bye, encode_error, encode_function_call, encode_function_call_output,
|
||||||
|
encode_hello, encode_pcm, encode_session_end, encode_speech_started, encode_speech_stopped,
|
||||||
|
encode_tools_update,
|
||||||
};
|
};
|
||||||
|
// Slice-3 additive (spec §3).
|
||||||
|
pub use protocol::{FunctionCallOutputPayload, FunctionCallPayload, ToolsUpdatePayload};
|
||||||
pub use tap_audio_pipe::TapAudioPipe;
|
pub use tap_audio_pipe::TapAudioPipe;
|
||||||
pub use tap_client::{TapClientError, run_tap_client};
|
pub use tap_client::{TapClientError, run_tap_client};
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,21 @@ pub enum FrameKind {
|
|||||||
SessionEnd,
|
SessionEnd,
|
||||||
Bye,
|
Bye,
|
||||||
Error,
|
Error,
|
||||||
|
/// brain → core: user speech started (advisory; translated from OpenAI
|
||||||
|
/// `input_audio_buffer.speech_started`; spec §3.2).
|
||||||
|
SpeechStarted,
|
||||||
|
/// brain → core: user speech stopped (advisory; OpenAI
|
||||||
|
/// `input_audio_buffer.speech_stopped`; spec §3.2).
|
||||||
|
SpeechStopped,
|
||||||
|
/// brain → core: brain proposes a tool call (translated from OpenAI
|
||||||
|
/// `response.function_call_arguments.done`; spec §3.2).
|
||||||
|
FunctionCall,
|
||||||
|
/// core → brain: tool registry reply (spec §3.3).
|
||||||
|
FunctionCallOutput,
|
||||||
|
/// brain → core: brain declares its tool catalog on hello + on changes
|
||||||
|
/// (spec §3.2).
|
||||||
|
#[serde(rename = "tools.update")]
|
||||||
|
ToolsUpdate,
|
||||||
/// Unknown wire `type` values land here (spec §3.4: log + count + drop).
|
/// Unknown wire `type` values land here (spec §3.4: log + count + drop).
|
||||||
/// `#[serde(other)]` catches any string not in the variants above.
|
/// `#[serde(other)]` catches any string not in the variants above.
|
||||||
#[serde(other)]
|
#[serde(other)]
|
||||||
@@ -111,6 +126,11 @@ impl Serialize for Envelope {
|
|||||||
Payload::AudioIn(_) | Payload::AudioOut(_) => 2, // pcm, samples
|
Payload::AudioIn(_) | Payload::AudioOut(_) => 2, // pcm, samples
|
||||||
Payload::SessionEnd(_) | Payload::Bye(_) => 1, // reason
|
Payload::SessionEnd(_) | Payload::Bye(_) => 1, // reason
|
||||||
Payload::Error(_) => 2, // code, message
|
Payload::Error(_) => 2, // code, message
|
||||||
|
// Slice-3 adds: empty payload (0 fields), 3-field payloads, 1-field payload.
|
||||||
|
Payload::SpeechStarted | Payload::SpeechStopped => 0,
|
||||||
|
Payload::FunctionCall(_) => 3, // id, name, args
|
||||||
|
Payload::FunctionCallOutput(_) => 3, // id, status, result
|
||||||
|
Payload::ToolsUpdate(_) => 1, // tools
|
||||||
};
|
};
|
||||||
let mut st = serializer.serialize_struct("Envelope", 4 + payload_field_count)?;
|
let mut st = serializer.serialize_struct("Envelope", 4 + payload_field_count)?;
|
||||||
st.serialize_field("v", &self.v)?;
|
st.serialize_field("v", &self.v)?;
|
||||||
@@ -142,6 +162,23 @@ impl Serialize for Envelope {
|
|||||||
st.serialize_field("code", &p.code)?;
|
st.serialize_field("code", &p.code)?;
|
||||||
st.serialize_field("message", &p.message)?;
|
st.serialize_field("message", &p.message)?;
|
||||||
}
|
}
|
||||||
|
Payload::SpeechStarted | Payload::SpeechStopped => {
|
||||||
|
// No payload fields — the envelope's `v`/`type`/`seq`/`ts`
|
||||||
|
// is the whole message. The event name IS the message.
|
||||||
|
}
|
||||||
|
Payload::FunctionCall(p) => {
|
||||||
|
st.serialize_field("id", &p.id)?;
|
||||||
|
st.serialize_field("name", &p.name)?;
|
||||||
|
st.serialize_field("args", &p.args)?;
|
||||||
|
}
|
||||||
|
Payload::FunctionCallOutput(p) => {
|
||||||
|
st.serialize_field("id", &p.id)?;
|
||||||
|
st.serialize_field("status", &p.status)?;
|
||||||
|
st.serialize_field("result", &p.result)?;
|
||||||
|
}
|
||||||
|
Payload::ToolsUpdate(p) => {
|
||||||
|
st.serialize_field("tools", &p.tools)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
st.end()
|
st.end()
|
||||||
}
|
}
|
||||||
@@ -165,6 +202,16 @@ pub enum Payload {
|
|||||||
SessionEnd(SessionEndPayload),
|
SessionEnd(SessionEndPayload),
|
||||||
Bye(ReasonPayload),
|
Bye(ReasonPayload),
|
||||||
Error(ErrorPayload),
|
Error(ErrorPayload),
|
||||||
|
/// Slice-3 additive (spec §3.2): brain → core, advisory; empty payload
|
||||||
|
/// (the event name IS the message).
|
||||||
|
SpeechStarted,
|
||||||
|
SpeechStopped,
|
||||||
|
/// Slice-3 additive (spec §3.2).
|
||||||
|
FunctionCall(FunctionCallPayload),
|
||||||
|
/// Slice-3 additive (spec §3.3): core → brain reply.
|
||||||
|
FunctionCallOutput(FunctionCallOutputPayload),
|
||||||
|
/// Slice-3 additive (spec §3.2): brain → core catalog declaration.
|
||||||
|
ToolsUpdate(ToolsUpdatePayload),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -211,6 +258,42 @@ pub struct ErrorPayload {
|
|||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `function_call` payload (brain → core; spec §3.2). Carries the
|
||||||
|
/// brain-minted id + tool name + args. Args is a raw JSON Value (not a
|
||||||
|
/// typed struct) so any tool schema is allowed.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct FunctionCallPayload {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
/// Raw JSON arguments — the tool registry dispatches by name and lets
|
||||||
|
/// each Tool impl parse the args itself. (OpenAI sends `arguments` as a
|
||||||
|
/// JSON string; the translator parses it back to a Value before
|
||||||
|
/// emitting the function_call tap frame.)
|
||||||
|
pub args: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `function_call_output` payload (core → brain; spec §3.3). The reply for
|
||||||
|
/// a `function_call`. `status` is one of `"ok"`, `"error"`, `"not_implemented"`.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct FunctionCallOutputPayload {
|
||||||
|
pub id: String,
|
||||||
|
pub status: String,
|
||||||
|
pub result: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `tools.update` payload (brain → core; spec §3.2). The brain declares its
|
||||||
|
/// tool catalog so the core's tool registry can validate function_call
|
||||||
|
/// events by name.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ToolsUpdatePayload {
|
||||||
|
/// An array of tool descriptors (each has `name`, `description`,
|
||||||
|
/// `parameters`). The shape is intentionally permissive (a JSON array,
|
||||||
|
/// not a typed Vec<ToolSchema>) so the brain can declare schemas the
|
||||||
|
/// core doesn't know about — the core only checks the `name` field for
|
||||||
|
/// dispatch, ignores the rest.
|
||||||
|
pub tools: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
/// Decoded frame — what `decode_envelope` returns. The kind is split into
|
/// Decoded frame — what `decode_envelope` returns. The kind is split into
|
||||||
/// `audio_in` / `audio_out` variants (not just `Audio(AudioPayload)`) so
|
/// `audio_in` / `audio_out` variants (not just `Audio(AudioPayload)`) so
|
||||||
/// callers `match` exhaustively on direction (spec §3.2 vs §3.3 — the two
|
/// callers `match` exhaustively on direction (spec §3.2 vs §3.3 — the two
|
||||||
@@ -231,7 +314,17 @@ pub enum DecodedPayload {
|
|||||||
SessionEnd(SessionEndPayload),
|
SessionEnd(SessionEndPayload),
|
||||||
Bye(ReasonPayload),
|
Bye(ReasonPayload),
|
||||||
Error(ErrorPayload),
|
Error(ErrorPayload),
|
||||||
/// Unknown `type` — log + count + drop (spec §3.4).
|
/// Slice-3 (spec §3.2): the brain detected user speech started/stopped.
|
||||||
|
SpeechStarted,
|
||||||
|
SpeechStopped,
|
||||||
|
/// Slice-3 (spec §3.2): brain wants the core to execute a tool.
|
||||||
|
FunctionCall(FunctionCallPayload),
|
||||||
|
/// Slice-3 (spec §3.3): the core's tool-registry reply.
|
||||||
|
FunctionCallOutput(FunctionCallOutputPayload),
|
||||||
|
/// Slice-3 (spec §3.2): brain declares its catalog so the core can
|
||||||
|
/// validate function_call events.
|
||||||
|
ToolsUpdate(ToolsUpdatePayload),
|
||||||
|
/// Unknown `type` — log + count + drop (spec §3.4 of slice-2).
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,6 +462,111 @@ pub fn encode_error(code: &str, msg: &str, seq: u64, ts: u64) -> Result<String,
|
|||||||
Ok(serde_json::to_string(&env)?)
|
Ok(serde_json::to_string(&env)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build `speech_started` (brain → core, advisory; spec §3.2).
|
||||||
|
pub fn encode_speech_started(seq: u64, ts: u64) -> Result<String, TapProtoError> {
|
||||||
|
let env = Envelope {
|
||||||
|
v: PROTOCOL_VERSION,
|
||||||
|
kind: FrameKind::SpeechStarted,
|
||||||
|
seq,
|
||||||
|
ts,
|
||||||
|
payload: Payload::SpeechStarted,
|
||||||
|
};
|
||||||
|
Ok(serde_json::to_string(&env)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build `speech_stopped` (brain → core, advisory; spec §3.2).
|
||||||
|
pub fn encode_speech_stopped(seq: u64, ts: u64) -> Result<String, TapProtoError> {
|
||||||
|
let env = Envelope {
|
||||||
|
v: PROTOCOL_VERSION,
|
||||||
|
kind: FrameKind::SpeechStopped,
|
||||||
|
seq,
|
||||||
|
ts,
|
||||||
|
payload: Payload::SpeechStopped,
|
||||||
|
};
|
||||||
|
Ok(serde_json::to_string(&env)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build `function_call` (brain → core; spec §3.2). `args_json_str` is the
|
||||||
|
/// raw JSON string the brain's translator parses from OpenAI's
|
||||||
|
/// `response.function_call_arguments.done.arguments` (which is itself a
|
||||||
|
/// JSON string in OpenAI's wire format).
|
||||||
|
pub fn encode_function_call(
|
||||||
|
id: &str,
|
||||||
|
name: &str,
|
||||||
|
args_json_str: &str,
|
||||||
|
seq: u64,
|
||||||
|
ts: u64,
|
||||||
|
) -> Result<String, TapProtoError> {
|
||||||
|
let args: serde_json::Value = if args_json_str.is_empty() {
|
||||||
|
serde_json::Value::Null
|
||||||
|
} else {
|
||||||
|
serde_json::from_str(args_json_str)?
|
||||||
|
};
|
||||||
|
let env = Envelope {
|
||||||
|
v: PROTOCOL_VERSION,
|
||||||
|
kind: FrameKind::FunctionCall,
|
||||||
|
seq,
|
||||||
|
ts,
|
||||||
|
payload: Payload::FunctionCall(FunctionCallPayload {
|
||||||
|
id: id.to_string(),
|
||||||
|
name: name.to_string(),
|
||||||
|
args,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
Ok(serde_json::to_string(&env)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build `function_call_output` (core → brain; spec §3.3). `result_json_str`
|
||||||
|
/// is the raw JSON string the tool-registry dispatch returns (serialized
|
||||||
|
/// into the result field of the payload).
|
||||||
|
pub fn encode_function_call_output(
|
||||||
|
id: &str,
|
||||||
|
status: &str, // "ok" | "error" | "not_implemented"
|
||||||
|
result_json_str: &str,
|
||||||
|
seq: u64,
|
||||||
|
ts: u64,
|
||||||
|
) -> Result<String, TapProtoError> {
|
||||||
|
let result: serde_json::Value = if result_json_str.is_empty() {
|
||||||
|
serde_json::Value::Null
|
||||||
|
} else {
|
||||||
|
serde_json::from_str(result_json_str)?
|
||||||
|
};
|
||||||
|
let env = Envelope {
|
||||||
|
v: PROTOCOL_VERSION,
|
||||||
|
kind: FrameKind::FunctionCallOutput,
|
||||||
|
seq,
|
||||||
|
ts,
|
||||||
|
payload: Payload::FunctionCallOutput(FunctionCallOutputPayload {
|
||||||
|
id: id.to_string(),
|
||||||
|
status: status.to_string(),
|
||||||
|
result,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
Ok(serde_json::to_string(&env)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build `tools.update` (brain → core; spec §3.2). `tools_json_str` is the
|
||||||
|
/// raw JSON array of tool descriptors.
|
||||||
|
pub fn encode_tools_update(
|
||||||
|
tools_json_str: &str,
|
||||||
|
seq: u64,
|
||||||
|
ts: u64,
|
||||||
|
) -> Result<String, TapProtoError> {
|
||||||
|
let tools: serde_json::Value = if tools_json_str.is_empty() {
|
||||||
|
serde_json::Value::Array(vec![])
|
||||||
|
} else {
|
||||||
|
serde_json::from_str(tools_json_str)?
|
||||||
|
};
|
||||||
|
let env = Envelope {
|
||||||
|
v: PROTOCOL_VERSION,
|
||||||
|
kind: FrameKind::ToolsUpdate,
|
||||||
|
seq,
|
||||||
|
ts,
|
||||||
|
payload: Payload::ToolsUpdate(ToolsUpdatePayload { tools }),
|
||||||
|
};
|
||||||
|
Ok(serde_json::to_string(&env)?)
|
||||||
|
}
|
||||||
|
|
||||||
/// Decode an incoming wire string into a `DecodedFrame`. Validates `v` and
|
/// Decode an incoming wire string into a `DecodedFrame`. Validates `v` and
|
||||||
/// (for audio frames) `samples` and PCM round-trip. Unknown `type` values
|
/// (for audio frames) `samples` and PCM round-trip. Unknown `type` values
|
||||||
/// decode to `DecodedPayload::Unknown` (caller logs + counts + drops per
|
/// decode to `DecodedPayload::Unknown` (caller logs + counts + drops per
|
||||||
@@ -440,6 +638,20 @@ pub fn decode_envelope(s: &str) -> Result<DecodedFrame, TapProtoError> {
|
|||||||
let p: ErrorPayload = serde_json::from_value(extra_value)?;
|
let p: ErrorPayload = serde_json::from_value(extra_value)?;
|
||||||
DecodedPayload::Error(p)
|
DecodedPayload::Error(p)
|
||||||
}
|
}
|
||||||
|
FrameKind::SpeechStarted => DecodedPayload::SpeechStarted,
|
||||||
|
FrameKind::SpeechStopped => DecodedPayload::SpeechStopped,
|
||||||
|
FrameKind::FunctionCall => {
|
||||||
|
let p: FunctionCallPayload = serde_json::from_value(extra_value)?;
|
||||||
|
DecodedPayload::FunctionCall(p)
|
||||||
|
}
|
||||||
|
FrameKind::FunctionCallOutput => {
|
||||||
|
let p: FunctionCallOutputPayload = serde_json::from_value(extra_value)?;
|
||||||
|
DecodedPayload::FunctionCallOutput(p)
|
||||||
|
}
|
||||||
|
FrameKind::ToolsUpdate => {
|
||||||
|
let p: ToolsUpdatePayload = serde_json::from_value(extra_value)?;
|
||||||
|
DecodedPayload::ToolsUpdate(p)
|
||||||
|
}
|
||||||
FrameKind::Unknown => DecodedPayload::Unknown,
|
FrameKind::Unknown => DecodedPayload::Unknown,
|
||||||
};
|
};
|
||||||
Ok(DecodedFrame {
|
Ok(DecodedFrame {
|
||||||
@@ -596,4 +808,96 @@ mod tests {
|
|||||||
}
|
}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Slice-3 additive event types must round-trip through (de)serialization
|
||||||
|
/// without breaking slice-2's v1 contract. Every new kind + payload.
|
||||||
|
#[test]
|
||||||
|
fn speech_started_round_trips() {
|
||||||
|
let s = encode_speech_started(7, 100).unwrap();
|
||||||
|
assert!(s.contains("\"type\":\"speech_started\""));
|
||||||
|
assert!(s.contains("\"v\":1"));
|
||||||
|
let d = decode_envelope(&s).unwrap();
|
||||||
|
assert_eq!(d.seq, 7);
|
||||||
|
assert_eq!(d.ts, 100);
|
||||||
|
assert!(matches!(d.payload, DecodedPayload::SpeechStarted));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn speech_stopped_round_trips() {
|
||||||
|
let s = encode_speech_stopped(9, 200).unwrap();
|
||||||
|
assert!(s.contains("\"type\":\"speech_stopped\""));
|
||||||
|
let d = decode_envelope(&s).unwrap();
|
||||||
|
assert_eq!(d.seq, 9);
|
||||||
|
assert!(matches!(d.payload, DecodedPayload::SpeechStopped));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_call_round_trips() {
|
||||||
|
let s = encode_function_call("abc-123", "hangup", "{}", 0, 0).unwrap();
|
||||||
|
assert!(s.contains("\"type\":\"function_call\""));
|
||||||
|
assert!(s.contains("\"id\":\"abc-123\""));
|
||||||
|
assert!(s.contains("\"name\":\"hangup\""));
|
||||||
|
let d = decode_envelope(&s).unwrap();
|
||||||
|
match d.payload {
|
||||||
|
DecodedPayload::FunctionCall(p) => {
|
||||||
|
assert_eq!(p.id, "abc-123");
|
||||||
|
assert_eq!(p.name, "hangup");
|
||||||
|
}
|
||||||
|
other => panic!("expected FunctionCall, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_call_output_round_trips() {
|
||||||
|
let s =
|
||||||
|
encode_function_call_output("abc-123", "ok", r#"{"channel_state":"Closing"}"#, 0, 0)
|
||||||
|
.unwrap();
|
||||||
|
assert!(s.contains("\"type\":\"function_call_output\""));
|
||||||
|
assert!(s.contains("\"status\":\"ok\""));
|
||||||
|
let d = decode_envelope(&s).unwrap();
|
||||||
|
match d.payload {
|
||||||
|
DecodedPayload::FunctionCallOutput(p) => {
|
||||||
|
assert_eq!(p.id, "abc-123");
|
||||||
|
assert_eq!(p.status, "ok");
|
||||||
|
}
|
||||||
|
other => panic!("expected FunctionCallOutput, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tools_update_round_trips() {
|
||||||
|
let tools_json = r#"[{"name":"hangup","description":"hang up the call"}]"#;
|
||||||
|
let s = encode_tools_update(tools_json, 0, 0).unwrap();
|
||||||
|
assert!(s.contains("\"type\":\"tools.update\""));
|
||||||
|
assert!(s.contains("\"tools\":["));
|
||||||
|
let d = decode_envelope(&s).unwrap();
|
||||||
|
match d.payload {
|
||||||
|
DecodedPayload::ToolsUpdate(p) => {
|
||||||
|
assert!(p.tools.is_array());
|
||||||
|
assert_eq!(p.tools.as_array().unwrap().len(), 1);
|
||||||
|
}
|
||||||
|
other => panic!("expected ToolsUpdate, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forwards-compat: slice-2's echo brain sees the new types as unknown
|
||||||
|
/// (the `#[serde(other)]` on the old enum absorbed them). With the new
|
||||||
|
/// enum in place, the kinds decode to their new variants (rather than
|
||||||
|
/// Unknown). This test asserts the decode no longer drops them.
|
||||||
|
#[test]
|
||||||
|
fn new_kinds_decode_to_their_variants_not_unknown() {
|
||||||
|
for s in [
|
||||||
|
encode_speech_started(0, 0).unwrap(),
|
||||||
|
encode_speech_stopped(0, 0).unwrap(),
|
||||||
|
encode_function_call("x", "x", "null", 0, 0).unwrap(),
|
||||||
|
encode_function_call_output("x", "ok", "null", 0, 0).unwrap(),
|
||||||
|
encode_tools_update("[]", 0, 0).unwrap(),
|
||||||
|
] {
|
||||||
|
let d = decode_envelope(&s).unwrap();
|
||||||
|
assert!(
|
||||||
|
!matches!(d.payload, DecodedPayload::Unknown),
|
||||||
|
"new event type decoded as Unknown: {s}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -307,6 +307,19 @@ async fn handle_brain_frame(
|
|||||||
metrics.unknown_frames.fetch_add(1, Ordering::Relaxed);
|
metrics.unknown_frames.fetch_add(1, Ordering::Relaxed);
|
||||||
warn!("unexpected frame direction from brain; dropping");
|
warn!("unexpected frame direction from brain; dropping");
|
||||||
}
|
}
|
||||||
|
// Slice-3 additive v1 event types (spec §3.2): forwards-compatible
|
||||||
|
// wire shapes the slice-2 tap client doesn't yet act on — the
|
||||||
|
// brain's translator (Task 4) emits these, the tool registry
|
||||||
|
// (Task 6) consumes them. Until those land, log + count + drop
|
||||||
|
// (same posture as `Unknown`, slice-2 §3.4).
|
||||||
|
DecodedPayload::SpeechStarted
|
||||||
|
| DecodedPayload::SpeechStopped
|
||||||
|
| DecodedPayload::FunctionCall(_)
|
||||||
|
| DecodedPayload::FunctionCallOutput(_)
|
||||||
|
| DecodedPayload::ToolsUpdate(_) => {
|
||||||
|
metrics.unknown_frames.fetch_add(1, Ordering::Relaxed);
|
||||||
|
debug!("slice-3 event type not yet handled; dropping");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let _ = session_start; // used for ts computation if added later
|
let _ = session_start; // used for ts computation if added later
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user