Chatapp Protobuf Schemas and Shared Plugins

Reference for Pinocchio chatapp protobuf contracts, segment-aware message entities, and shared reasoning/tool-call plugins.

Sections

Terminology & Glossary
📖 Documentation
Navigation
54 sectionsv0.1
📄 Chatapp Protobuf Schemas and Shared Plugins — glaze help chatapp-protobuf-plugins
chatapp-protobuf-plugins

Chatapp Protobuf Schemas and Shared Plugins

Reference for Pinocchio chatapp protobuf contracts, segment-aware message entities, and shared reasoning/tool-call plugins.

Topicwebchatchatappprotobufsessionstreampluginsweb-chat

Overview

pinocchio/pkg/chatapp is the reusable chat application layer on top of sessionstream. It owns the base chat command, event, UI-event, and timeline schemas, while application packages can add more behavior through ChatPlugin implementations.

The current chatapp contract is protobuf-first:

  • source schema: proto/pinocchio/chatapp/v1/chat.proto
  • generated Go package: pkg/chatapp/pb/proto/pinocchio/chatapp/v1
  • generator config: buf.chatapp.gen.yaml
  • runtime registration: chatapp.RegisterSchemas(reg, plugins...)

Sessionstream still delivers browser frames as JSON over WebSocket, but those frames are the JSON form of registered protobuf messages. Backend code should publish concrete, feature-owned proto.Message payloads, not ad-hoc maps or generic google.protobuf.Struct objects.

Schema policy: concrete protobuf for every durable contract

Every chatapp/sessionstream command, backend event, UI event, and timeline entity payload must have its own concrete protobuf message, including app-specific features. Treat the protobuf type as the durable API contract between:

  • runtime event producers;
  • sessionstream projection code;
  • persisted timeline snapshots;
  • WebSocket JSON frames;
  • frontend renderers and generated TypeScript types.

Do not register google.protobuf.Struct for event/UI/timeline payloads just because a feature is app-local. App-local still means durable once it is persisted, hydrated, or rendered after reload.

Good:

reg.RegisterUIEvent("ChatAgentModeCommitted", &chatappv1.AgentModeCommittedUpdate{})
reg.RegisterTimelineEntity("AgentMode", &chatappv1.AgentModeEntity{})

Avoid:

reg.RegisterUIEvent("ChatAgentModeCommitted", &structpb.Struct{})
reg.RegisterTimelineEntity("AgentMode", &structpb.Struct{})

Use google.protobuf.Struct only inside a typed message field when the field is intentionally open-ended metadata, for example provider-specific debug details or arbitrary tool input/output. The outer payload registered with sessionstream should still be a named protobuf message.

A repository-level architecture test (pkg/chatapp/schema_policy_test.go) and vet analyzer (pkg/analysis/sessionstreamschema, runnable with make schema-vet) reject RegisterEvent, RegisterUIEvent, and RegisterTimelineEntity registrations that use &structpb.Struct{}.

Base chatapp schema

The base chatapp registers these command messages:

NameProtobuf messagePurpose
ChatStartInferenceStartInferenceCommandStart an assistant run for a prompt.
ChatStopInferenceStopInferenceCommandStop the active assistant run for a session.

It registers these backend events and UI events with ChatMessageUpdate payloads:

Backend eventUI eventPurpose
ChatUserMessageAcceptedChatMessageAcceptedUser prompt was accepted and projected into the timeline.
ChatInferenceStartedChatMessageStartedAssistant run started.
ChatTokensDeltaChatMessageAppendedAssistant text changed during streaming.
ChatInferenceFinishedChatMessageFinishedAssistant text segment or final response finished.
ChatInferenceStoppedChatMessageStoppedAssistant run stopped or failed.

It registers one base timeline entity kind:

KindProtobuf messagePurpose
ChatMessageChatMessageEntityUser, assistant, thinking, and warning transcript rows.

Segment-aware transcript rows

ChatMessageUpdate and ChatMessageEntity include segment metadata:

FieldMeaning
message_id / JSON messageIdStable entity ID for this concrete transcript row.
parent_message_id / JSON parentMessageIdAssistant run ID that owns this row, when the row is a segment.
segmentOne-based segment number within the parent assistant run.
segment_type / JSON segmentTypeLogical segment kind, for example text or thinking.
finalTrue only for the final assistant text row of the run.

This is important for tool loops. A single assistant run can produce:

chat-msg-1:thinking:1
chat-msg-1:text:2
ChatToolCall / ChatToolResult rows
chat-msg-1:thinking:3
chat-msg-1:text:4

Timeline stores and Redux reducers upsert by entity ID. Therefore every distinct transcript row must have a distinct messageId. Do not reuse the parent assistant ID for multiple thinking blocks or for multiple interleaved assistant text blocks.

Shared ReasoningPlugin

pkg/chatapp/plugins.NewReasoningPlugin() translates Geppetto reasoning events into chatapp/sessionstream events.

It handles:

  • *events.EventThinkingPartial
  • *events.EventInfo with thinking-started
  • *events.EventInfo with thinking-ended
  • reasoning summary info payloads when available

It registers concrete ReasoningUpdate protobuf payloads:

Backend eventUI eventPayload
ChatReasoningStartedChatReasoningStartedReasoningUpdate
ChatReasoningDeltaChatReasoningAppendedReasoningUpdate
ChatReasoningFinishedChatReasoningFinishedReasoningUpdate

The payload contains chat-message-shaped fields such as messageId, parentMessageId, segment, role: "thinking", chunk, content, status, and streaming. Each contiguous thinking phase gets a segment ID such as chat-msg-5:thinking:1.

Use this shared plugin instead of defining app-local runtime-debug/reasoning projection code.

Shared ToolCallPlugin

pkg/chatapp/plugins.NewToolCallPlugin() translates Geppetto tool lifecycle events into typed protobuf payloads.

It handles:

  • *events.EventToolCall
  • *events.EventToolCallExecute
  • *events.EventToolResult
  • *events.EventToolCallExecutionResult

It registers:

Backend event / UI eventPayload
ChatToolCallStartedToolCallUpdate
ChatToolCallUpdatedToolCallUpdate
ChatToolCallFinishedToolCallUpdate
ChatToolResultReadyToolResultUpdate

It also registers timeline entity kinds:

KindPayload
ChatToolCallToolCallEntity
ChatToolResultToolResultEntity

Use this shared plugin for apps that want durable, hydrated tool-call and tool-result rows. Product-specific tools can still add their own widgets, but they should not duplicate the generic Geppetto tool lifecycle projection.

Wiring pattern

A web-chat style application wires the base schemas and plugins at server assembly time:

import (
    chatapp "github.com/go-go-golems/pinocchio/pkg/chatapp"
    chatplugins "github.com/go-go-golems/pinocchio/pkg/chatapp/plugins"
    sessionstream "github.com/go-go-golems/sessionstream/pkg/sessionstream"
)

reg := sessionstream.NewSchemaRegistry()
chatPlugins := []chatapp.ChatPlugin{
    myAppSpecificPlugin(),
    chatplugins.NewReasoningPlugin(),
    chatplugins.NewToolCallPlugin(),
}

if err := chatapp.RegisterSchemas(reg, chatPlugins...); err != nil {
    return err
}

engine := chatapp.NewEngine(chatapp.WithPlugins(chatPlugins...))

The reference app uses this pattern from cmd/web-chat/main.go and cmd/web-chat/app/server.go. agentmode remains app-owned under cmd/web-chat; reasoning and tool calls are reusable chatapp plugins under pkg/chatapp/plugins.

Frontend implications

The Pinocchio web frontend receives canonical sessionstream frames from /api/chat/ws and maps them into local renderer keys:

  • ChatMessage snapshot entities become local message entities.
  • thinking rows are represented as message entities with role: "thinking".
  • app-specific entities such as AgentMode keep their own renderer path.

Downstream applications may choose different frontend mappings. The stable contract is the sessionstream event/entity name plus the protobuf payload, not the local React component name.

Troubleshooting

ProblemCauseSolution
Reasoning appears as one overwritten blockMultiple thinking phases reused the same entity IDUse ReasoningPlugin and preserve segment-aware messageId values.
Tool calls stream live but disappear after reloadTool calls were only local UI stateRegister and use ToolCallPlugin so tool calls project into timeline entities.
Protobuf payload cannot be decodedSchema was not registered with the sessionstream registryRegister base chatapp schemas and every plugin before creating the hub.
Frontend sees unknown entity kindsBackend registered new timeline kinds without frontend renderersAdd a renderer or normalize the entity into an existing local renderer kind.

See Also