Mercurial
comparison mrjunejune/inference_bridge.c @ 260:1f9877b637e9
Add Copilot-powered cyberpunk JRPG chat
Integrate the production JRPG chat with Seobeo streaming, Deita persistence, and a Bazel-managed Copilot SDK and LiteLLM inference stack.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <mrjunejune@users.noreply.github.com> |
|---|---|
| date | Wed, 05 Aug 2026 09:19:41 -0700 |
| parents | |
| children | b401627fc49e |
comparison
equal
deleted
inserted
replaced
| 259:667156fcd3e3 | 260:1f9877b637e9 |
|---|---|
| 1 #include "mrjunejune/inference_bridge.h" | |
| 2 | |
| 3 #include <errno.h> | |
| 4 #include <pthread.h> | |
| 5 #include <signal.h> | |
| 6 #include <stdatomic.h> | |
| 7 #include <stdio.h> | |
| 8 #include <stdlib.h> | |
| 9 #include <string.h> | |
| 10 #include <sys/wait.h> | |
| 11 #include <time.h> | |
| 12 #include <unistd.h> | |
| 13 | |
| 14 #define INFERENCE_EVENT_MAX (1024 * 1024 + 4096) | |
| 15 #define INFERENCE_READY_TIMEOUT_MS 15000 | |
| 16 | |
| 17 struct Inference_Bridge { | |
| 18 char *sidecar_path; | |
| 19 char *copilot_cli_path; | |
| 20 FILE *p_commands; | |
| 21 FILE *p_events; | |
| 22 pid_t child_pid; | |
| 23 pthread_t reader_thread; | |
| 24 pthread_mutex_t write_mutex; | |
| 25 pthread_mutex_t ready_mutex; | |
| 26 pthread_cond_t ready_condition; | |
| 27 boolean write_mutex_initialized; | |
| 28 boolean ready_mutex_initialized; | |
| 29 boolean ready_condition_initialized; | |
| 30 boolean reader_started; | |
| 31 _Atomic boolean running; | |
| 32 _Atomic boolean ready; | |
| 33 Inference_Event_Handler handler; | |
| 34 void *p_user_data; | |
| 35 }; | |
| 36 | |
| 37 static int64 Inference_Monotonic_Milliseconds(void) | |
| 38 { | |
| 39 struct timespec now; | |
| 40 clock_gettime(CLOCK_MONOTONIC, &now); | |
| 41 return (int64)now.tv_sec * 1000 + now.tv_nsec / 1000000; | |
| 42 } | |
| 43 | |
| 44 static const char *Inference_JSON_String( | |
| 45 Dowa_JSON_Entry *object, | |
| 46 const char *key) | |
| 47 { | |
| 48 char *value = Dowa_JSON_Get_String(object, key); | |
| 49 return value ? value : ""; | |
| 50 } | |
| 51 | |
| 52 static boolean Inference_JSON_Boolean( | |
| 53 Dowa_JSON_Entry *object, | |
| 54 const char *key) | |
| 55 { | |
| 56 Dowa_JSON_Value *p_value = Dowa_JSON_Get(object, key); | |
| 57 return p_value && p_value->type == DOWA_JSON_BOOL | |
| 58 ? p_value->bool_val | |
| 59 : FALSE; | |
| 60 } | |
| 61 | |
| 62 static int64 Inference_JSON_Integer( | |
| 63 Dowa_JSON_Entry *object, | |
| 64 const char *key) | |
| 65 { | |
| 66 Dowa_JSON_Value *p_value = Dowa_JSON_Get(object, key); | |
| 67 return p_value && p_value->type == DOWA_JSON_NUMBER | |
| 68 ? (int64)p_value->num_val | |
| 69 : 0; | |
| 70 } | |
| 71 | |
| 72 static void Inference_Bridge_Handle_Line( | |
| 73 Inference_Bridge *p_bridge, | |
| 74 const char *line, | |
| 75 size_t length) | |
| 76 { | |
| 77 Dowa_Arena *p_arena = Dowa_Arena_Create(length * 3 + 4096); | |
| 78 if (!p_arena) | |
| 79 return; | |
| 80 Dowa_JSON_Value parsed = Dowa_JSON_Parse(line, (int32)length, p_arena); | |
| 81 if (parsed.type != DOWA_JSON_OBJECT) | |
| 82 { | |
| 83 Dowa_Arena_Free(p_arena); | |
| 84 return; | |
| 85 } | |
| 86 Dowa_JSON_Entry *object = parsed.object_val; | |
| 87 const char *type = Inference_JSON_String(object, "type"); | |
| 88 if (strcmp(type, "ready") == 0) | |
| 89 { | |
| 90 atomic_store(&p_bridge->ready, TRUE); | |
| 91 pthread_mutex_lock(&p_bridge->ready_mutex); | |
| 92 pthread_cond_broadcast(&p_bridge->ready_condition); | |
| 93 pthread_mutex_unlock(&p_bridge->ready_mutex); | |
| 94 } | |
| 95 | |
| 96 Inference_Event event = { | |
| 97 .type = type, | |
| 98 .request_id = Inference_JSON_String(object, "request_id"), | |
| 99 .conversation_id = Inference_JSON_String(object, "conversation_id"), | |
| 100 .delta = Inference_JSON_String(object, "delta"), | |
| 101 .content = Inference_JSON_String(object, "content"), | |
| 102 .failed = Inference_JSON_Boolean(object, "failed"), | |
| 103 .aborted = Inference_JSON_Boolean(object, "aborted"), | |
| 104 }; | |
| 105 | |
| 106 Dowa_JSON_Value *p_error = Dowa_JSON_Get(object, "error"); | |
| 107 if (p_error && p_error->type == DOWA_JSON_OBJECT) | |
| 108 { | |
| 109 Dowa_JSON_Entry *error = p_error->object_val; | |
| 110 event.error_code = Inference_JSON_String(error, "code"); | |
| 111 event.error_message = Inference_JSON_String(error, "message"); | |
| 112 } | |
| 113 else | |
| 114 { | |
| 115 event.error_code = ""; | |
| 116 event.error_message = ""; | |
| 117 } | |
| 118 | |
| 119 Dowa_JSON_Value *p_usage = Dowa_JSON_Get(object, "usage"); | |
| 120 if (p_usage && p_usage->type == DOWA_JSON_OBJECT) | |
| 121 { | |
| 122 Dowa_JSON_Entry *usage = p_usage->object_val; | |
| 123 event.input_tokens = Inference_JSON_Integer(usage, "input_tokens"); | |
| 124 event.output_tokens = Inference_JSON_Integer(usage, "output_tokens"); | |
| 125 } | |
| 126 | |
| 127 if (p_bridge->handler) | |
| 128 p_bridge->handler(&event, p_bridge->p_user_data); | |
| 129 Dowa_Arena_Free(p_arena); | |
| 130 } | |
| 131 | |
| 132 static void *Inference_Bridge_Read_Events(void *p_context) | |
| 133 { | |
| 134 Inference_Bridge *p_bridge = p_context; | |
| 135 char *line = NULL; | |
| 136 size_t capacity = 0; | |
| 137 while (atomic_load(&p_bridge->running)) | |
| 138 { | |
| 139 ssize_t amount = getline(&line, &capacity, p_bridge->p_events); | |
| 140 if (amount < 0) | |
| 141 break; | |
| 142 if ((size_t)amount > INFERENCE_EVENT_MAX) | |
| 143 continue; | |
| 144 size_t length = (size_t)amount; | |
| 145 while (length > 0 && | |
| 146 (line[length - 1] == '\n' || line[length - 1] == '\r')) | |
| 147 length--; | |
| 148 if (length > 0) | |
| 149 Inference_Bridge_Handle_Line(p_bridge, line, length); | |
| 150 } | |
| 151 free(line); | |
| 152 atomic_store(&p_bridge->ready, FALSE); | |
| 153 atomic_store(&p_bridge->running, FALSE); | |
| 154 if (p_bridge->handler) | |
| 155 { | |
| 156 Inference_Event closed = { | |
| 157 .type = "bridge.closed", | |
| 158 .error_code = "sidecar_closed", | |
| 159 .error_message = "Inference sidecar connection closed", | |
| 160 .failed = TRUE, | |
| 161 }; | |
| 162 p_bridge->handler(&closed, p_bridge->p_user_data); | |
| 163 } | |
| 164 pthread_mutex_lock(&p_bridge->ready_mutex); | |
| 165 pthread_cond_broadcast(&p_bridge->ready_condition); | |
| 166 pthread_mutex_unlock(&p_bridge->ready_mutex); | |
| 167 return NULL; | |
| 168 } | |
| 169 | |
| 170 static boolean Inference_Bridge_Write( | |
| 171 Inference_Bridge *p_bridge, | |
| 172 const char *payload) | |
| 173 { | |
| 174 if (!p_bridge || !payload || !atomic_load(&p_bridge->running)) | |
| 175 return FALSE; | |
| 176 pthread_mutex_lock(&p_bridge->write_mutex); | |
| 177 boolean success = | |
| 178 fputs(payload, p_bridge->p_commands) >= 0 && | |
| 179 fputc('\n', p_bridge->p_commands) != EOF && | |
| 180 fflush(p_bridge->p_commands) == 0; | |
| 181 pthread_mutex_unlock(&p_bridge->write_mutex); | |
| 182 return success; | |
| 183 } | |
| 184 | |
| 185 static boolean Inference_Bridge_Command( | |
| 186 Inference_Bridge *p_bridge, | |
| 187 const char *command, | |
| 188 const char *request_id, | |
| 189 const char *conversation_id, | |
| 190 const char *prompt) | |
| 191 { | |
| 192 if (!command || !request_id) | |
| 193 return FALSE; | |
| 194 size_t input_length = | |
| 195 strlen(command) + strlen(request_id) + | |
| 196 strlen(conversation_id ? conversation_id : "") + | |
| 197 strlen(prompt ? prompt : ""); | |
| 198 if (input_length > (((size_t)-1) - 4096) / 12) | |
| 199 return FALSE; | |
| 200 Dowa_Arena *p_arena = Dowa_Arena_Create(input_length * 12 + 4096); | |
| 201 if (!p_arena) | |
| 202 return FALSE; | |
| 203 char *escaped_command = Dowa_JSON_Escape_String(command, 0, p_arena); | |
| 204 char *escaped_request = Dowa_JSON_Escape_String(request_id, 0, p_arena); | |
| 205 char *escaped_conversation = Dowa_JSON_Escape_String( | |
| 206 conversation_id ? conversation_id : "", 0, p_arena); | |
| 207 char *escaped_prompt = prompt | |
| 208 ? Dowa_JSON_Escape_String(prompt, 0, p_arena) | |
| 209 : NULL; | |
| 210 if (!escaped_command || !escaped_request || !escaped_conversation || | |
| 211 (prompt && !escaped_prompt)) | |
| 212 { | |
| 213 Dowa_Arena_Free(p_arena); | |
| 214 return FALSE; | |
| 215 } | |
| 216 size_t capacity = | |
| 217 strlen(escaped_command) + strlen(escaped_request) + | |
| 218 strlen(escaped_conversation) + | |
| 219 (escaped_prompt ? strlen(escaped_prompt) : 0) + 160; | |
| 220 char *payload = Dowa_Arena_Allocate(p_arena, capacity); | |
| 221 if (!payload) | |
| 222 { | |
| 223 Dowa_Arena_Free(p_arena); | |
| 224 return FALSE; | |
| 225 } | |
| 226 if (escaped_prompt) | |
| 227 { | |
| 228 snprintf( | |
| 229 payload, | |
| 230 capacity, | |
| 231 "{\"command\":\"%s\",\"request_id\":\"%s\"," | |
| 232 "\"conversation_id\":\"%s\",\"prompt\":\"%s\"}", | |
| 233 escaped_command, | |
| 234 escaped_request, | |
| 235 escaped_conversation, | |
| 236 escaped_prompt); | |
| 237 } | |
| 238 else | |
| 239 { | |
| 240 snprintf( | |
| 241 payload, | |
| 242 capacity, | |
| 243 "{\"command\":\"%s\",\"request_id\":\"%s\"," | |
| 244 "\"conversation_id\":\"%s\"}", | |
| 245 escaped_command, | |
| 246 escaped_request, | |
| 247 escaped_conversation); | |
| 248 } | |
| 249 boolean success = Inference_Bridge_Write(p_bridge, payload); | |
| 250 Dowa_Arena_Free(p_arena); | |
| 251 return success; | |
| 252 } | |
| 253 | |
| 254 Inference_Bridge *Inference_Bridge_Create( | |
| 255 const char *sidecar_path, | |
| 256 const char *copilot_cli_path, | |
| 257 Inference_Event_Handler handler, | |
| 258 void *p_user_data) | |
| 259 { | |
| 260 if (!sidecar_path || !copilot_cli_path) | |
| 261 return NULL; | |
| 262 Inference_Bridge *p_bridge = calloc(1, sizeof(*p_bridge)); | |
| 263 if (!p_bridge) | |
| 264 return NULL; | |
| 265 p_bridge->sidecar_path = strdup(sidecar_path); | |
| 266 p_bridge->copilot_cli_path = strdup(copilot_cli_path); | |
| 267 p_bridge->handler = handler; | |
| 268 p_bridge->p_user_data = p_user_data; | |
| 269 p_bridge->child_pid = -1; | |
| 270 if (!p_bridge->sidecar_path || !p_bridge->copilot_cli_path) | |
| 271 { | |
| 272 Inference_Bridge_Destroy(p_bridge); | |
| 273 return NULL; | |
| 274 } | |
| 275 if (pthread_mutex_init(&p_bridge->write_mutex, NULL) != 0) | |
| 276 { | |
| 277 Inference_Bridge_Destroy(p_bridge); | |
| 278 return NULL; | |
| 279 } | |
| 280 p_bridge->write_mutex_initialized = TRUE; | |
| 281 if (pthread_mutex_init(&p_bridge->ready_mutex, NULL) != 0) | |
| 282 { | |
| 283 Inference_Bridge_Destroy(p_bridge); | |
| 284 return NULL; | |
| 285 } | |
| 286 p_bridge->ready_mutex_initialized = TRUE; | |
| 287 if (pthread_cond_init(&p_bridge->ready_condition, NULL) != 0) | |
| 288 { | |
| 289 Inference_Bridge_Destroy(p_bridge); | |
| 290 return NULL; | |
| 291 } | |
| 292 p_bridge->ready_condition_initialized = TRUE; | |
| 293 return p_bridge; | |
| 294 } | |
| 295 | |
| 296 static void Inference_Bridge_Stop_Process(Inference_Bridge *p_bridge) | |
| 297 { | |
| 298 if (!p_bridge) | |
| 299 return; | |
| 300 if (atomic_load(&p_bridge->running) && p_bridge->p_commands) | |
| 301 Inference_Bridge_Command( | |
| 302 p_bridge, "shutdown", "server-shutdown", "", NULL); | |
| 303 if (p_bridge->p_commands) | |
| 304 { | |
| 305 fclose(p_bridge->p_commands); | |
| 306 p_bridge->p_commands = NULL; | |
| 307 } | |
| 308 | |
| 309 if (p_bridge->child_pid > 0) | |
| 310 { | |
| 311 int status = 0; | |
| 312 pid_t result = 0; | |
| 313 for (int attempt = 0; attempt < 20; attempt++) | |
| 314 { | |
| 315 result = waitpid(p_bridge->child_pid, &status, WNOHANG); | |
| 316 if (result != 0) | |
| 317 break; | |
| 318 usleep(50000); | |
| 319 } | |
| 320 if (result == 0) | |
| 321 { | |
| 322 kill(p_bridge->child_pid, SIGTERM); | |
| 323 waitpid(p_bridge->child_pid, &status, 0); | |
| 324 } | |
| 325 p_bridge->child_pid = -1; | |
| 326 } | |
| 327 atomic_store(&p_bridge->running, FALSE); | |
| 328 atomic_store(&p_bridge->ready, FALSE); | |
| 329 if (p_bridge->reader_started) | |
| 330 { | |
| 331 pthread_join(p_bridge->reader_thread, NULL); | |
| 332 p_bridge->reader_started = FALSE; | |
| 333 } | |
| 334 if (p_bridge->p_events) | |
| 335 { | |
| 336 fclose(p_bridge->p_events); | |
| 337 p_bridge->p_events = NULL; | |
| 338 } | |
| 339 } | |
| 340 | |
| 341 boolean Inference_Bridge_Start(Inference_Bridge *p_bridge) | |
| 342 { | |
| 343 if (!p_bridge || atomic_load(&p_bridge->running)) | |
| 344 return FALSE; | |
| 345 int commands[2]; | |
| 346 int events[2]; | |
| 347 if (pipe(commands) != 0) | |
| 348 return FALSE; | |
| 349 if (pipe(events) != 0) | |
| 350 { | |
| 351 close(commands[0]); | |
| 352 close(commands[1]); | |
| 353 return FALSE; | |
| 354 } | |
| 355 | |
| 356 pid_t child = fork(); | |
| 357 if (child < 0) | |
| 358 { | |
| 359 close(commands[0]); | |
| 360 close(commands[1]); | |
| 361 close(events[0]); | |
| 362 close(events[1]); | |
| 363 return FALSE; | |
| 364 } | |
| 365 if (child == 0) | |
| 366 { | |
| 367 dup2(commands[0], STDIN_FILENO); | |
| 368 dup2(events[1], STDOUT_FILENO); | |
| 369 close(commands[0]); | |
| 370 close(commands[1]); | |
| 371 close(events[0]); | |
| 372 close(events[1]); | |
| 373 execl( | |
| 374 p_bridge->sidecar_path, | |
| 375 p_bridge->sidecar_path, | |
| 376 p_bridge->copilot_cli_path, | |
| 377 NULL); | |
| 378 _exit(127); | |
| 379 } | |
| 380 | |
| 381 close(commands[0]); | |
| 382 close(events[1]); | |
| 383 p_bridge->p_commands = fdopen(commands[1], "w"); | |
| 384 p_bridge->p_events = fdopen(events[0], "r"); | |
| 385 if (!p_bridge->p_commands || !p_bridge->p_events) | |
| 386 { | |
| 387 if (p_bridge->p_commands) | |
| 388 fclose(p_bridge->p_commands); | |
| 389 else | |
| 390 close(commands[1]); | |
| 391 if (p_bridge->p_events) | |
| 392 fclose(p_bridge->p_events); | |
| 393 else | |
| 394 close(events[0]); | |
| 395 kill(child, SIGTERM); | |
| 396 waitpid(child, NULL, 0); | |
| 397 return FALSE; | |
| 398 } | |
| 399 setvbuf(p_bridge->p_commands, NULL, _IOLBF, 0); | |
| 400 p_bridge->child_pid = child; | |
| 401 atomic_store(&p_bridge->running, TRUE); | |
| 402 atomic_store(&p_bridge->ready, FALSE); | |
| 403 if (pthread_create( | |
| 404 &p_bridge->reader_thread, | |
| 405 NULL, | |
| 406 Inference_Bridge_Read_Events, | |
| 407 p_bridge) != 0) | |
| 408 { | |
| 409 atomic_store(&p_bridge->running, FALSE); | |
| 410 fclose(p_bridge->p_commands); | |
| 411 fclose(p_bridge->p_events); | |
| 412 kill(child, SIGTERM); | |
| 413 waitpid(child, NULL, 0); | |
| 414 p_bridge->p_commands = NULL; | |
| 415 p_bridge->p_events = NULL; | |
| 416 p_bridge->child_pid = -1; | |
| 417 return FALSE; | |
| 418 } | |
| 419 p_bridge->reader_started = TRUE; | |
| 420 | |
| 421 int64 deadline = | |
| 422 Inference_Monotonic_Milliseconds() + INFERENCE_READY_TIMEOUT_MS; | |
| 423 pthread_mutex_lock(&p_bridge->ready_mutex); | |
| 424 while (atomic_load(&p_bridge->running) && | |
| 425 !atomic_load(&p_bridge->ready)) | |
| 426 { | |
| 427 int64 remaining = deadline - Inference_Monotonic_Milliseconds(); | |
| 428 if (remaining <= 0) | |
| 429 break; | |
| 430 struct timespec timeout; | |
| 431 clock_gettime(CLOCK_REALTIME, &timeout); | |
| 432 timeout.tv_sec += remaining / 1000; | |
| 433 timeout.tv_nsec += (remaining % 1000) * 1000000; | |
| 434 if (timeout.tv_nsec >= 1000000000) | |
| 435 { | |
| 436 timeout.tv_sec++; | |
| 437 timeout.tv_nsec -= 1000000000; | |
| 438 } | |
| 439 pthread_cond_timedwait( | |
| 440 &p_bridge->ready_condition, &p_bridge->ready_mutex, &timeout); | |
| 441 } | |
| 442 boolean ready = atomic_load(&p_bridge->ready); | |
| 443 pthread_mutex_unlock(&p_bridge->ready_mutex); | |
| 444 if (!ready) | |
| 445 { | |
| 446 Inference_Bridge_Stop_Process(p_bridge); | |
| 447 return FALSE; | |
| 448 } | |
| 449 return TRUE; | |
| 450 } | |
| 451 | |
| 452 boolean Inference_Bridge_Is_Ready(const Inference_Bridge *p_bridge) | |
| 453 { | |
| 454 return p_bridge && | |
| 455 atomic_load(&p_bridge->running) && | |
| 456 atomic_load(&p_bridge->ready); | |
| 457 } | |
| 458 | |
| 459 boolean Inference_Bridge_Start_Turn( | |
| 460 Inference_Bridge *p_bridge, | |
| 461 const char *request_id, | |
| 462 const char *conversation_id, | |
| 463 const char *prompt) | |
| 464 { | |
| 465 return Inference_Bridge_Command( | |
| 466 p_bridge, "turn.start", request_id, conversation_id, prompt); | |
| 467 } | |
| 468 | |
| 469 boolean Inference_Bridge_Abort_Turn( | |
| 470 Inference_Bridge *p_bridge, | |
| 471 const char *request_id, | |
| 472 const char *conversation_id) | |
| 473 { | |
| 474 return Inference_Bridge_Command( | |
| 475 p_bridge, "turn.abort", request_id, conversation_id, NULL); | |
| 476 } | |
| 477 | |
| 478 boolean Inference_Bridge_Delete_Conversation( | |
| 479 Inference_Bridge *p_bridge, | |
| 480 const char *request_id, | |
| 481 const char *conversation_id) | |
| 482 { | |
| 483 return Inference_Bridge_Command( | |
| 484 p_bridge, "conversation.delete", request_id, conversation_id, NULL); | |
| 485 } | |
| 486 | |
| 487 void Inference_Bridge_Destroy(Inference_Bridge *p_bridge) | |
| 488 { | |
| 489 if (!p_bridge) | |
| 490 return; | |
| 491 Inference_Bridge_Stop_Process(p_bridge); | |
| 492 if (p_bridge->ready_condition_initialized) | |
| 493 pthread_cond_destroy(&p_bridge->ready_condition); | |
| 494 if (p_bridge->ready_mutex_initialized) | |
| 495 pthread_mutex_destroy(&p_bridge->ready_mutex); | |
| 496 if (p_bridge->write_mutex_initialized) | |
| 497 pthread_mutex_destroy(&p_bridge->write_mutex); | |
| 498 free(p_bridge->sidecar_path); | |
| 499 free(p_bridge->copilot_cli_path); | |
| 500 free(p_bridge); | |
| 501 } |