add: some proto stuff

This commit is contained in:
2026-03-24 21:11:45 -07:00
parent 3df04e4cb6
commit 622eca78cb
10 changed files with 1752 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
node_modules/
+23
View File
@@ -0,0 +1,23 @@
.PHONY: proto proto-deps help
# Default target
all: help
# Generate Go code from proto files using buf
proto: proto-deps
@echo "Generating Go code from proto files..."
buf generate || exit 1
@echo "Proto generation complete!"
# Ensure buf is available
proto-deps:
@which buf >/dev/null 2>&1 || (echo "Error: buf is not installed. Please install Buf first." && exit 1)
# Help target
help:
@echo "Webstory Makefile"
@echo ""
@echo "Available targets:"
@echo " make all - Show this help message"
@echo " make proto - Generate Go code from proto files"
@echo " make proto-deps - Ensure buf is installed"
+2
View File
@@ -65,6 +65,8 @@ pkg/ollama -- manages access to the Ollama backend
pkg/api -- contains code implementing the webstory API
The backend is implemented in gRPC, but exposes a traditional REST API.
### Frontend
All frontend code resides in web/.
+8
View File
@@ -0,0 +1,8 @@
version: v1
plugins:
- name: go
out: pkg/api
opt: paths=source_relative
- name: go-grpc
out: pkg/api
opt: paths=source_relative
+10
View File
@@ -0,0 +1,10 @@
# For details on buf.yaml configuration, visit https://buf.build/docs/configuration/v2/buf-yaml
version: v2
lint:
use:
- STANDARD
breaking:
use:
- FILE
modules:
- path: proto
+14
View File
@@ -1,3 +1,17 @@
module git.tipsy.codes/charles/webstory
go 1.26.1
require (
google.golang.org/grpc v1.70.0
google.golang.org/protobuf v1.36.11
)
require (
go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
)
+34
View File
@@ -0,0 +1,34 @@
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
File diff suppressed because it is too large Load Diff
+291
View File
@@ -0,0 +1,291 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc (unknown)
// source: webstory/v1/api.proto
package api
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
WebstoryService_StartStory_FullMethodName = "/webstory.v1.WebstoryService/StartStory"
WebstoryService_ContinueStory_FullMethodName = "/webstory.v1.WebstoryService/ContinueStory"
WebstoryService_GetStory_FullMethodName = "/webstory.v1.WebstoryService/GetStory"
WebstoryService_ResetStory_FullMethodName = "/webstory.v1.WebstoryService/ResetStory"
WebstoryService_StreamStory_FullMethodName = "/webstory.v1.WebstoryService/StreamStory"
)
// WebstoryServiceClient is the client API for WebstoryService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// WebstoryService defines the RPC methods for the interactive fiction service
type WebstoryServiceClient interface {
// Start a new story session
StartStory(ctx context.Context, in *StartStoryRequest, opts ...grpc.CallOption) (*StartStoryResponse, error)
// Continue the story with user input or choice
ContinueStory(ctx context.Context, in *ContinueStoryRequest, opts ...grpc.CallOption) (*ContinueStoryResponse, error)
// Get the current state of a story
GetStory(ctx context.Context, in *GetStoryRequest, opts ...grpc.CallOption) (*GetStoryResponse, error)
// Reset or terminate a story session
ResetStory(ctx context.Context, in *ResetStoryRequest, opts ...grpc.CallOption) (*ResetStoryResponse, error)
// Stream story responses in real-time
StreamStory(ctx context.Context, in *StreamStoryRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StreamStoryResponse], error)
}
type webstoryServiceClient struct {
cc grpc.ClientConnInterface
}
func NewWebstoryServiceClient(cc grpc.ClientConnInterface) WebstoryServiceClient {
return &webstoryServiceClient{cc}
}
func (c *webstoryServiceClient) StartStory(ctx context.Context, in *StartStoryRequest, opts ...grpc.CallOption) (*StartStoryResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(StartStoryResponse)
err := c.cc.Invoke(ctx, WebstoryService_StartStory_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *webstoryServiceClient) ContinueStory(ctx context.Context, in *ContinueStoryRequest, opts ...grpc.CallOption) (*ContinueStoryResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ContinueStoryResponse)
err := c.cc.Invoke(ctx, WebstoryService_ContinueStory_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *webstoryServiceClient) GetStory(ctx context.Context, in *GetStoryRequest, opts ...grpc.CallOption) (*GetStoryResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetStoryResponse)
err := c.cc.Invoke(ctx, WebstoryService_GetStory_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *webstoryServiceClient) ResetStory(ctx context.Context, in *ResetStoryRequest, opts ...grpc.CallOption) (*ResetStoryResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ResetStoryResponse)
err := c.cc.Invoke(ctx, WebstoryService_ResetStory_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *webstoryServiceClient) StreamStory(ctx context.Context, in *StreamStoryRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StreamStoryResponse], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &WebstoryService_ServiceDesc.Streams[0], WebstoryService_StreamStory_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[StreamStoryRequest, StreamStoryResponse]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type WebstoryService_StreamStoryClient = grpc.ServerStreamingClient[StreamStoryResponse]
// WebstoryServiceServer is the server API for WebstoryService service.
// All implementations must embed UnimplementedWebstoryServiceServer
// for forward compatibility.
//
// WebstoryService defines the RPC methods for the interactive fiction service
type WebstoryServiceServer interface {
// Start a new story session
StartStory(context.Context, *StartStoryRequest) (*StartStoryResponse, error)
// Continue the story with user input or choice
ContinueStory(context.Context, *ContinueStoryRequest) (*ContinueStoryResponse, error)
// Get the current state of a story
GetStory(context.Context, *GetStoryRequest) (*GetStoryResponse, error)
// Reset or terminate a story session
ResetStory(context.Context, *ResetStoryRequest) (*ResetStoryResponse, error)
// Stream story responses in real-time
StreamStory(*StreamStoryRequest, grpc.ServerStreamingServer[StreamStoryResponse]) error
mustEmbedUnimplementedWebstoryServiceServer()
}
// UnimplementedWebstoryServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedWebstoryServiceServer struct{}
func (UnimplementedWebstoryServiceServer) StartStory(context.Context, *StartStoryRequest) (*StartStoryResponse, error) {
return nil, status.Error(codes.Unimplemented, "method StartStory not implemented")
}
func (UnimplementedWebstoryServiceServer) ContinueStory(context.Context, *ContinueStoryRequest) (*ContinueStoryResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ContinueStory not implemented")
}
func (UnimplementedWebstoryServiceServer) GetStory(context.Context, *GetStoryRequest) (*GetStoryResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetStory not implemented")
}
func (UnimplementedWebstoryServiceServer) ResetStory(context.Context, *ResetStoryRequest) (*ResetStoryResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ResetStory not implemented")
}
func (UnimplementedWebstoryServiceServer) StreamStory(*StreamStoryRequest, grpc.ServerStreamingServer[StreamStoryResponse]) error {
return status.Error(codes.Unimplemented, "method StreamStory not implemented")
}
func (UnimplementedWebstoryServiceServer) mustEmbedUnimplementedWebstoryServiceServer() {}
func (UnimplementedWebstoryServiceServer) testEmbeddedByValue() {}
// UnsafeWebstoryServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to WebstoryServiceServer will
// result in compilation errors.
type UnsafeWebstoryServiceServer interface {
mustEmbedUnimplementedWebstoryServiceServer()
}
func RegisterWebstoryServiceServer(s grpc.ServiceRegistrar, srv WebstoryServiceServer) {
// If the following call panics, it indicates UnimplementedWebstoryServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&WebstoryService_ServiceDesc, srv)
}
func _WebstoryService_StartStory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StartStoryRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WebstoryServiceServer).StartStory(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WebstoryService_StartStory_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WebstoryServiceServer).StartStory(ctx, req.(*StartStoryRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WebstoryService_ContinueStory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ContinueStoryRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WebstoryServiceServer).ContinueStory(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WebstoryService_ContinueStory_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WebstoryServiceServer).ContinueStory(ctx, req.(*ContinueStoryRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WebstoryService_GetStory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetStoryRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WebstoryServiceServer).GetStory(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WebstoryService_GetStory_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WebstoryServiceServer).GetStory(ctx, req.(*GetStoryRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WebstoryService_ResetStory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ResetStoryRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WebstoryServiceServer).ResetStory(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WebstoryService_ResetStory_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WebstoryServiceServer).ResetStory(ctx, req.(*ResetStoryRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WebstoryService_StreamStory_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(StreamStoryRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(WebstoryServiceServer).StreamStory(m, &grpc.GenericServerStream[StreamStoryRequest, StreamStoryResponse]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type WebstoryService_StreamStoryServer = grpc.ServerStreamingServer[StreamStoryResponse]
// WebstoryService_ServiceDesc is the grpc.ServiceDesc for WebstoryService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var WebstoryService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "webstory.v1.WebstoryService",
HandlerType: (*WebstoryServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "StartStory",
Handler: _WebstoryService_StartStory_Handler,
},
{
MethodName: "ContinueStory",
Handler: _WebstoryService_ContinueStory_Handler,
},
{
MethodName: "GetStory",
Handler: _WebstoryService_GetStory_Handler,
},
{
MethodName: "ResetStory",
Handler: _WebstoryService_ResetStory_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "StreamStory",
Handler: _WebstoryService_StreamStory_Handler,
ServerStreams: true,
},
},
Metadata: "webstory/v1/api.proto",
}
+138
View File
@@ -0,0 +1,138 @@
syntax = "proto3";
package webstory.v1;
import "google/protobuf/timestamp.proto";
option go_package = "git.tipsy.codes/charles/webstory/pkg/api";
// Story represents the current state of an interactive fiction session
message Story {
string session_id = 1; // Unique identifier for this story session
string current_scene = 2; // Current scene/location identifier
string status = 3; // Current story status (playing, paused, completed)
}
// Visualization represents an image or visual element to display
message Visualization {
string type = 1; // Type of visualization: portrait, scene, map, etc.
string url = 2; // URL or path to the visualization
string description = 3; // Description of what should be shown
}
// Choice represents an option the user can select
message Choice {
string label = 1; // Text displayed on the button
string action = 2; // Action to trigger when selected
map<string, string> metadata = 3; // Additional metadata (optional)
}
// Message represents a communication between client and server
message Message {
string id = 1; // Unique message identifier
string type = 2; // Message type: user_input, story_text, system, etc.
string content = 3; // Message content
repeated Choice choices = 4; // Available choices (if any)
Visualization visualization = 5; // Associated visualization (if any)
google.protobuf.Timestamp created_at = 6; // Timestamp when message was created
google.protobuf.Timestamp updated_at = 7; // Timestamp when message was last updated
}
// ChatHistory represents the conversation history
message ChatHistory {
string session_id = 1;
repeated Message messages = 2;
google.protobuf.Timestamp created_at = 3; // Timestamp when chat history was created
google.protobuf.Timestamp updated_at = 4; // Timestamp when chat history was last updated
}
// Request for starting a new story
message StartStoryRequest {
string session_id = 1; // Optional: client-provided session ID
string initial_scene = 2; // Starting scene or scenario prompt
string story_genre = 3; // Genre preferences (fantasy, sci-fi, etc.)
map<string, string> options = 4; // Additional configuration options
google.protobuf.Timestamp created_at = 5; // Timestamp when request was created
}
// Response for starting a story
message StartStoryResponse {
string session_id = 1;
string status = 2;
Message initial_message = 3;
map<string, string> metadata = 4;
google.protobuf.Timestamp created_at = 5; // Timestamp when response was created
}
// Request for continuing the story
message ContinueStoryRequest {
string session_id = 1;
string action = 2; // User's choice or input
string choice_id = 3; // If selecting a specific choice
map<string, string> options = 4; // Optional parameters
google.protobuf.Timestamp created_at = 5; // Timestamp when request was created
}
// Response for continuing the story
message ContinueStoryResponse {
string session_id = 1;
string status = 2;
Message response_message = 3;
map<string, string> metadata = 4;
}
// Request to get story state
message GetStoryRequest {
string session_id = 1;
}
// Response with current story state
message GetStoryResponse {
string session_id = 1;
Story story = 2;
repeated Message recent_messages = 3;
}
// Request to reset/stop a story
message ResetStoryRequest {
string session_id = 1;
}
message ResetStoryResponse {
string session_id = 1;
string status = 2;
}
// WebstoryService defines the RPC methods for the interactive fiction service
service WebstoryService {
// Start a new story session
rpc StartStory(StartStoryRequest) returns (StartStoryResponse);
// Continue the story with user input or choice
rpc ContinueStory(ContinueStoryRequest) returns (ContinueStoryResponse);
// Get the current state of a story
rpc GetStory(GetStoryRequest) returns (GetStoryResponse);
// Reset or terminate a story session
rpc ResetStory(ResetStoryRequest) returns (ResetStoryResponse);
// Stream story responses in real-time
rpc StreamStory(StreamStoryRequest) returns (stream StreamStoryResponse);
}
// Request for streaming story responses
message StreamStoryRequest {
string session_id = 1;
string action = 2;
string choice_id = 3;
map<string, string> options = 4;
}
// Response for streaming story responses
message StreamStoryResponse {
string session_id = 1;
Message partial_message = 2; // Partial message being built
Message complete_message = 3; // Complete message when finalized
bool is_final = 4; // Indicates if this is the final chunk
}