Mercurial
comparison infinite_canvas/agent_service_copilot.c @ 280:49e9e591c9bb
Add persistent dictation, prewarmed WebRTC speech input, Copilot SDK routing, animated conversation lifecycle controls, parking, and architecture coverage.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Tue, 18 Aug 2026 19:14:53 -0700 |
| parents | |
| children | c57149ad216e |
comparison
equal
deleted
inserted
replaced
| 279:b3b547563ec7 | 280:49e9e591c9bb |
|---|---|
| 1 #include "infinite_canvas/agent_service.h" | |
| 2 | |
| 3 #include "mrjunejune/inference_bridge.h" | |
| 4 | |
| 5 #include <pthread.h> | |
| 6 #include <stdio.h> | |
| 7 #include <stdlib.h> | |
| 8 #include <string.h> | |
| 9 #include <unistd.h> | |
| 10 | |
| 11 #define CANVAS_AGENT_WARM_SESSION_COUNT 4 | |
| 12 | |
| 13 typedef struct { | |
| 14 pthread_mutex_t mutex; | |
| 15 Canvas_Agent_Service *p_service; | |
| 16 Inference_Bridge *p_bridge; | |
| 17 uint32 request_sequence; | |
| 18 uint32 warmed_sessions; | |
| 19 char request_id[64]; | |
| 20 char turn_content[CANVAS_AGENT_RESPONSE_CAPACITY]; | |
| 21 char turn_error[512]; | |
| 22 char turn_prompt[ | |
| 23 CANVAS_CONTEXT_MAX_LENGTH + CANVAS_ENTITY_TEXT_CAPACITY + 512]; | |
| 24 } Canvas_Agent_Copilot; | |
| 25 | |
| 26 static boolean Canvas_Agent_Event_Matches( | |
| 27 const Canvas_Agent_Copilot *p_copilot, | |
| 28 const Inference_Event *p_event) | |
| 29 { | |
| 30 return p_event->request_id && | |
| 31 strcmp(p_copilot->request_id, p_event->request_id) == 0; | |
| 32 } | |
| 33 | |
| 34 static void Canvas_Agent_Handle_Event( | |
| 35 const Inference_Event *p_event, | |
| 36 void *p_user_data) | |
| 37 { | |
| 38 Canvas_Agent_Copilot *p_copilot = | |
| 39 (Canvas_Agent_Copilot *)p_user_data; | |
| 40 if (!p_copilot || !p_event) return; | |
| 41 | |
| 42 pthread_mutex_lock(&p_copilot->mutex); | |
| 43 if (strcmp(p_event->type, "bridge.closed") == 0) { | |
| 44 if (p_copilot->p_service->status == CANVAS_AGENT_WORKING) { | |
| 45 snprintf( | |
| 46 p_copilot->p_service->error, | |
| 47 sizeof(p_copilot->p_service->error), | |
| 48 "%s", | |
| 49 p_event->error_message && p_event->error_message[0] | |
| 50 ? p_event->error_message | |
| 51 : "Copilot orchestration sidecar closed"); | |
| 52 p_copilot->p_service->status = CANVAS_AGENT_ERROR; | |
| 53 } | |
| 54 pthread_mutex_unlock(&p_copilot->mutex); | |
| 55 return; | |
| 56 } | |
| 57 if (strcmp(p_event->type, "session.warmed") == 0 && | |
| 58 p_event->request_id && | |
| 59 strncmp(p_event->request_id, "canvas-warm-", 12) == 0) { | |
| 60 p_copilot->warmed_sessions++; | |
| 61 pthread_mutex_unlock(&p_copilot->mutex); | |
| 62 return; | |
| 63 } | |
| 64 if (!Canvas_Agent_Event_Matches(p_copilot, p_event)) { | |
| 65 pthread_mutex_unlock(&p_copilot->mutex); | |
| 66 return; | |
| 67 } | |
| 68 | |
| 69 if (strcmp(p_event->type, "assistant.completed") == 0) { | |
| 70 snprintf( | |
| 71 p_copilot->turn_content, | |
| 72 sizeof(p_copilot->turn_content), | |
| 73 "%s", | |
| 74 p_event->content ? p_event->content : ""); | |
| 75 } else if (strcmp(p_event->type, "turn.error") == 0) { | |
| 76 snprintf( | |
| 77 p_copilot->turn_error, | |
| 78 sizeof(p_copilot->turn_error), | |
| 79 "%s", | |
| 80 p_event->error_message && p_event->error_message[0] | |
| 81 ? p_event->error_message | |
| 82 : "Copilot orchestration failed"); | |
| 83 } else if (strcmp(p_event->type, "turn.done") == 0) { | |
| 84 if (p_event->failed || p_copilot->turn_error[0]) { | |
| 85 snprintf( | |
| 86 p_copilot->p_service->error, | |
| 87 sizeof(p_copilot->p_service->error), | |
| 88 "%s", | |
| 89 p_copilot->turn_error[0] | |
| 90 ? p_copilot->turn_error | |
| 91 : "Copilot orchestration failed"); | |
| 92 p_copilot->p_service->status = CANVAS_AGENT_ERROR; | |
| 93 } else if (p_copilot->turn_content[0]) { | |
| 94 snprintf( | |
| 95 p_copilot->p_service->response, | |
| 96 sizeof(p_copilot->p_service->response), | |
| 97 "%s", | |
| 98 p_copilot->turn_content); | |
| 99 p_copilot->p_service->status = CANVAS_AGENT_READY; | |
| 100 } else { | |
| 101 snprintf( | |
| 102 p_copilot->p_service->error, | |
| 103 sizeof(p_copilot->p_service->error), | |
| 104 "Copilot returned an empty response"); | |
| 105 p_copilot->p_service->status = CANVAS_AGENT_ERROR; | |
| 106 } | |
| 107 } | |
| 108 pthread_mutex_unlock(&p_copilot->mutex); | |
| 109 } | |
| 110 | |
| 111 boolean Canvas_Agent_Service_Init( | |
| 112 Canvas_Agent_Service *p_service, | |
| 113 Dowa_Arena *p_arena) | |
| 114 { | |
| 115 memset(p_service, 0, sizeof(*p_service)); | |
| 116 p_service->p_arena = p_arena; | |
| 117 const char *p_sidecar = | |
| 118 getenv("INFINITE_CANVAS_COPILOT_SIDECAR_PATH"); | |
| 119 const char *p_cli = | |
| 120 getenv("INFINITE_CANVAS_COPILOT_CLI_PATH"); | |
| 121 if (!p_sidecar || !p_sidecar[0] || !p_cli || !p_cli[0]) { | |
| 122 return FALSE; | |
| 123 } | |
| 124 | |
| 125 Canvas_Agent_Copilot *p_copilot = (Canvas_Agent_Copilot *) | |
| 126 Dowa_Arena_Allocate(p_arena, sizeof(*p_copilot)); | |
| 127 if (!p_copilot) return FALSE; | |
| 128 memset(p_copilot, 0, sizeof(*p_copilot)); | |
| 129 if (pthread_mutex_init(&p_copilot->mutex, NULL) != 0) { | |
| 130 return FALSE; | |
| 131 } | |
| 132 p_copilot->p_service = p_service; | |
| 133 p_copilot->p_bridge = Inference_Bridge_Create( | |
| 134 p_sidecar, | |
| 135 p_cli, | |
| 136 Canvas_Agent_Handle_Event, | |
| 137 p_copilot); | |
| 138 if (!p_copilot->p_bridge || | |
| 139 !Inference_Bridge_Start(p_copilot->p_bridge)) { | |
| 140 if (p_copilot->p_bridge) { | |
| 141 Inference_Bridge_Destroy(p_copilot->p_bridge); | |
| 142 } | |
| 143 pthread_mutex_destroy(&p_copilot->mutex); | |
| 144 return FALSE; | |
| 145 } | |
| 146 const char *warm_sessions[CANVAS_AGENT_WARM_SESSION_COUNT] = { | |
| 147 "infinite-canvas-orchestrator", | |
| 148 "infinite-canvas-worker-1", | |
| 149 "infinite-canvas-worker-2", | |
| 150 "infinite-canvas-worker-3", | |
| 151 }; | |
| 152 for (uint32 index = 0; | |
| 153 index < CANVAS_AGENT_WARM_SESSION_COUNT; | |
| 154 index++) { | |
| 155 char request_id[32]; | |
| 156 snprintf( | |
| 157 request_id, | |
| 158 sizeof(request_id), | |
| 159 "canvas-warm-%u", | |
| 160 index); | |
| 161 if (!Inference_Bridge_Warm_Conversation( | |
| 162 p_copilot->p_bridge, | |
| 163 request_id, | |
| 164 warm_sessions[index], | |
| 165 INFERENCE_PROMPT_PROFILE_CANVAS_ORCHESTRATOR, | |
| 166 1, | |
| 167 1, | |
| 168 index == 0 ? TRUE : FALSE)) { | |
| 169 Inference_Bridge_Destroy(p_copilot->p_bridge); | |
| 170 pthread_mutex_destroy(&p_copilot->mutex); | |
| 171 return FALSE; | |
| 172 } | |
| 173 } | |
| 174 for (int32 attempt = 0; attempt < 2000; attempt++) { | |
| 175 pthread_mutex_lock(&p_copilot->mutex); | |
| 176 boolean ready = | |
| 177 p_copilot->warmed_sessions == CANVAS_AGENT_WARM_SESSION_COUNT; | |
| 178 pthread_mutex_unlock(&p_copilot->mutex); | |
| 179 if (ready) break; | |
| 180 usleep(10000); | |
| 181 } | |
| 182 p_service->p_native = p_copilot; | |
| 183 p_service->status = CANVAS_AGENT_IDLE; | |
| 184 return TRUE; | |
| 185 } | |
| 186 | |
| 187 boolean Canvas_Agent_Service_Submit( | |
| 188 Canvas_Agent_Service *p_service, | |
| 189 const char *p_prompt, | |
| 190 const char *p_context) | |
| 191 { | |
| 192 Canvas_Agent_Copilot *p_copilot = | |
| 193 (Canvas_Agent_Copilot *)p_service->p_native; | |
| 194 if (!p_copilot || !p_prompt || !p_prompt[0]) return FALSE; | |
| 195 | |
| 196 pthread_mutex_lock(&p_copilot->mutex); | |
| 197 if (p_service->status == CANVAS_AGENT_WORKING) { | |
| 198 pthread_mutex_unlock(&p_copilot->mutex); | |
| 199 return FALSE; | |
| 200 } | |
| 201 p_copilot->request_sequence++; | |
| 202 snprintf( | |
| 203 p_copilot->request_id, | |
| 204 sizeof(p_copilot->request_id), | |
| 205 "canvas-%u", | |
| 206 p_copilot->request_sequence); | |
| 207 snprintf( | |
| 208 p_copilot->turn_prompt, | |
| 209 sizeof(p_copilot->turn_prompt), | |
| 210 "Visible canvas context:\n%s\n\nNew user thought:\n%s", | |
| 211 p_context ? p_context : "", | |
| 212 p_prompt); | |
| 213 p_copilot->turn_content[0] = '\0'; | |
| 214 p_copilot->turn_error[0] = '\0'; | |
| 215 p_service->response[0] = '\0'; | |
| 216 p_service->error[0] = '\0'; | |
| 217 p_service->status = CANVAS_AGENT_WORKING; | |
| 218 char request_id[sizeof(p_copilot->request_id)]; | |
| 219 snprintf(request_id, sizeof(request_id), "%s", p_copilot->request_id); | |
| 220 pthread_mutex_unlock(&p_copilot->mutex); | |
| 221 | |
| 222 if (!Inference_Bridge_Start_Turn( | |
| 223 p_copilot->p_bridge, | |
| 224 request_id, | |
| 225 "infinite-canvas-orchestrator", | |
| 226 p_copilot->turn_prompt, | |
| 227 INFERENCE_PROMPT_PROFILE_CANVAS_ORCHESTRATOR, | |
| 228 1, | |
| 229 1, | |
| 230 NULL, | |
| 231 0)) { | |
| 232 pthread_mutex_lock(&p_copilot->mutex); | |
| 233 snprintf( | |
| 234 p_service->error, | |
| 235 sizeof(p_service->error), | |
| 236 "Unable to start the Copilot orchestration turn"); | |
| 237 p_service->status = CANVAS_AGENT_ERROR; | |
| 238 pthread_mutex_unlock(&p_copilot->mutex); | |
| 239 return FALSE; | |
| 240 } | |
| 241 return TRUE; | |
| 242 } | |
| 243 | |
| 244 Canvas_Agent_Status Canvas_Agent_Service_Poll( | |
| 245 Canvas_Agent_Service *p_service, | |
| 246 char *p_response, | |
| 247 size_t response_capacity, | |
| 248 char *p_error, | |
| 249 size_t error_capacity) | |
| 250 { | |
| 251 Canvas_Agent_Copilot *p_copilot = | |
| 252 (Canvas_Agent_Copilot *)p_service->p_native; | |
| 253 if (!p_copilot) return CANVAS_AGENT_ERROR; | |
| 254 pthread_mutex_lock(&p_copilot->mutex); | |
| 255 Canvas_Agent_Status status = p_service->status; | |
| 256 if (status == CANVAS_AGENT_READY) { | |
| 257 snprintf(p_response, response_capacity, "%s", p_service->response); | |
| 258 p_service->status = CANVAS_AGENT_IDLE; | |
| 259 } else if (status == CANVAS_AGENT_ERROR) { | |
| 260 snprintf(p_error, error_capacity, "%s", p_service->error); | |
| 261 p_service->status = CANVAS_AGENT_IDLE; | |
| 262 } | |
| 263 pthread_mutex_unlock(&p_copilot->mutex); | |
| 264 return status; | |
| 265 } | |
| 266 | |
| 267 void Canvas_Agent_Service_Shutdown(Canvas_Agent_Service *p_service) | |
| 268 { | |
| 269 Canvas_Agent_Copilot *p_copilot = | |
| 270 (Canvas_Agent_Copilot *)p_service->p_native; | |
| 271 if (!p_copilot) return; | |
| 272 Inference_Bridge_Destroy(p_copilot->p_bridge); | |
| 273 p_copilot->p_bridge = NULL; | |
| 274 pthread_mutex_destroy(&p_copilot->mutex); | |
| 275 p_service->p_native = NULL; | |
| 276 } |