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 }