pragma-so 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/index.ts +882 -0
- package/index.ts +3 -0
- package/package.json +53 -0
- package/server/connectorBinaries.ts +103 -0
- package/server/connectorRegistry.ts +158 -0
- package/server/conversation/adapterRegistry.ts +53 -0
- package/server/conversation/adapters/claudeAdapter.ts +138 -0
- package/server/conversation/adapters/codexAdapter.ts +142 -0
- package/server/conversation/adapters.ts +224 -0
- package/server/conversation/executeRunner.ts +1191 -0
- package/server/conversation/gitWorkflow.ts +1037 -0
- package/server/conversation/models.ts +23 -0
- package/server/conversation/pragmaCli.ts +34 -0
- package/server/conversation/prompts.ts +335 -0
- package/server/conversation/store.ts +805 -0
- package/server/conversation/titleGenerator.ts +106 -0
- package/server/conversation/turnRunner.ts +365 -0
- package/server/conversation/types.ts +134 -0
- package/server/db.ts +837 -0
- package/server/http/middleware.ts +31 -0
- package/server/http/schemas.ts +430 -0
- package/server/http/validators.ts +38 -0
- package/server/index.ts +6560 -0
- package/server/process/runCommand.ts +142 -0
- package/server/stores/agentStore.ts +167 -0
- package/server/stores/connectorStore.ts +299 -0
- package/server/stores/humanStore.ts +28 -0
- package/server/stores/skillStore.ts +127 -0
- package/server/stores/taskStore.ts +371 -0
- package/shared/net.ts +24 -0
- package/tsconfig.json +14 -0
- package/ui/index.html +14 -0
- package/ui/public/favicon-32.png +0 -0
- package/ui/public/favicon.png +0 -0
- package/ui/src/App.jsx +1338 -0
- package/ui/src/api.js +954 -0
- package/ui/src/components/CodeView.jsx +319 -0
- package/ui/src/components/ConnectionsView.jsx +1004 -0
- package/ui/src/components/ContextView.jsx +315 -0
- package/ui/src/components/ConversationDrawer.jsx +963 -0
- package/ui/src/components/EmptyPane.jsx +20 -0
- package/ui/src/components/FeedView.jsx +773 -0
- package/ui/src/components/FilesView.jsx +257 -0
- package/ui/src/components/InlineChatView.jsx +158 -0
- package/ui/src/components/InputBar.jsx +476 -0
- package/ui/src/components/OnboardingModal.jsx +112 -0
- package/ui/src/components/OutputPanel.jsx +658 -0
- package/ui/src/components/PlanProposalPanel.jsx +177 -0
- package/ui/src/components/RightPanel.jsx +951 -0
- package/ui/src/components/SettingsView.jsx +186 -0
- package/ui/src/components/Sidebar.jsx +247 -0
- package/ui/src/components/TestingPane.jsx +198 -0
- package/ui/src/components/testing/ApiTesterPanel.jsx +187 -0
- package/ui/src/components/testing/LogViewerPanel.jsx +64 -0
- package/ui/src/components/testing/TerminalPanel.jsx +104 -0
- package/ui/src/components/testing/WebPreviewPanel.jsx +78 -0
- package/ui/src/hooks/useAgents.js +81 -0
- package/ui/src/hooks/useConversation.js +252 -0
- package/ui/src/hooks/useTasks.js +161 -0
- package/ui/src/hooks/useWorkspace.js +259 -0
- package/ui/src/lib/agentIcon.js +10 -0
- package/ui/src/lib/conversationUtils.js +575 -0
- package/ui/src/main.jsx +10 -0
- package/ui/src/styles.css +6899 -0
- package/ui/vite.config.mjs +6 -0
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
import type { PGlite } from "@electric-sql/pglite";
|
|
2
|
+
import type {
|
|
3
|
+
ChatThreadListItem,
|
|
4
|
+
ConversationEvent,
|
|
5
|
+
ConversationMessage,
|
|
6
|
+
ConversationMode,
|
|
7
|
+
ConversationThread,
|
|
8
|
+
ConversationTurn,
|
|
9
|
+
HarnessId,
|
|
10
|
+
OpenPlanThreadListItem,
|
|
11
|
+
ReasoningEffort,
|
|
12
|
+
} from "./types";
|
|
13
|
+
|
|
14
|
+
export async function ensureConversationSchema(db: PGlite): Promise<void> {
|
|
15
|
+
await db.exec(`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS conversation_threads (
|
|
17
|
+
id VARCHAR(64) PRIMARY KEY,
|
|
18
|
+
mode VARCHAR(16) NOT NULL,
|
|
19
|
+
status VARCHAR(32) NOT NULL DEFAULT 'open',
|
|
20
|
+
harness VARCHAR(32) NOT NULL,
|
|
21
|
+
model_label VARCHAR(128) NOT NULL,
|
|
22
|
+
model_id VARCHAR(128) NOT NULL,
|
|
23
|
+
harness_session_id VARCHAR(255),
|
|
24
|
+
task_id VARCHAR(64),
|
|
25
|
+
source_thread_id VARCHAR(64),
|
|
26
|
+
chat_title TEXT,
|
|
27
|
+
chat_preview TEXT,
|
|
28
|
+
chat_last_message_at TIMESTAMPTZ,
|
|
29
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
30
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE TABLE IF NOT EXISTS conversation_turns (
|
|
34
|
+
id VARCHAR(64) PRIMARY KEY,
|
|
35
|
+
thread_id VARCHAR(64) NOT NULL REFERENCES conversation_threads(id) ON DELETE CASCADE,
|
|
36
|
+
mode VARCHAR(16) NOT NULL,
|
|
37
|
+
user_message TEXT NOT NULL,
|
|
38
|
+
assistant_message TEXT,
|
|
39
|
+
reasoning_effort VARCHAR(16),
|
|
40
|
+
requested_recipient_agent_id VARCHAR(64),
|
|
41
|
+
selected_agent_id VARCHAR(64),
|
|
42
|
+
orchestrator_agent_id VARCHAR(64),
|
|
43
|
+
worker_session_id VARCHAR(255),
|
|
44
|
+
selection_status VARCHAR(32),
|
|
45
|
+
status VARCHAR(32) NOT NULL DEFAULT 'running',
|
|
46
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
47
|
+
completed_at TIMESTAMPTZ
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
CREATE TABLE IF NOT EXISTS conversation_messages (
|
|
51
|
+
id VARCHAR(64) PRIMARY KEY,
|
|
52
|
+
thread_id VARCHAR(64) NOT NULL REFERENCES conversation_threads(id) ON DELETE CASCADE,
|
|
53
|
+
turn_id VARCHAR(64) REFERENCES conversation_turns(id) ON DELETE SET NULL,
|
|
54
|
+
role VARCHAR(16) NOT NULL,
|
|
55
|
+
content TEXT NOT NULL,
|
|
56
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
CREATE TABLE IF NOT EXISTS conversation_events (
|
|
60
|
+
id VARCHAR(64) PRIMARY KEY,
|
|
61
|
+
seq SERIAL,
|
|
62
|
+
thread_id VARCHAR(64) NOT NULL REFERENCES conversation_threads(id) ON DELETE CASCADE,
|
|
63
|
+
turn_id VARCHAR(64) REFERENCES conversation_turns(id) ON DELETE SET NULL,
|
|
64
|
+
event_name VARCHAR(64) NOT NULL,
|
|
65
|
+
payload_json TEXT NOT NULL,
|
|
66
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_conversation_turns_thread ON conversation_turns(thread_id, created_at DESC);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_conversation_messages_thread ON conversation_messages(thread_id, created_at ASC);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_conversation_events_thread ON conversation_events(thread_id, created_at ASC);
|
|
72
|
+
`);
|
|
73
|
+
|
|
74
|
+
await db.exec(`
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_conversation_chat_mode_sort ON conversation_threads(mode, created_at DESC);
|
|
76
|
+
`);
|
|
77
|
+
|
|
78
|
+
// Migration: add seq column to conversation_events if it doesn't exist
|
|
79
|
+
await db.exec(`
|
|
80
|
+
DO $$
|
|
81
|
+
BEGIN
|
|
82
|
+
IF NOT EXISTS (
|
|
83
|
+
SELECT 1 FROM information_schema.columns
|
|
84
|
+
WHERE table_name = 'conversation_events' AND column_name = 'seq'
|
|
85
|
+
) THEN
|
|
86
|
+
ALTER TABLE conversation_events ADD COLUMN seq SERIAL;
|
|
87
|
+
CREATE INDEX idx_conversation_events_seq ON conversation_events(thread_id, seq ASC);
|
|
88
|
+
END IF;
|
|
89
|
+
END $$;
|
|
90
|
+
`);
|
|
91
|
+
|
|
92
|
+
// Ensure index exists for seq-based queries even on fresh DBs
|
|
93
|
+
await db.exec(`
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_conversation_events_seq ON conversation_events(thread_id, seq ASC);
|
|
95
|
+
`);
|
|
96
|
+
|
|
97
|
+
// Migration: add plan_proposal_json column to conversation_turns if it doesn't exist
|
|
98
|
+
await db.exec(`
|
|
99
|
+
DO $$
|
|
100
|
+
BEGIN
|
|
101
|
+
IF NOT EXISTS (
|
|
102
|
+
SELECT 1 FROM information_schema.columns
|
|
103
|
+
WHERE table_name = 'conversation_turns' AND column_name = 'plan_proposal_json'
|
|
104
|
+
) THEN
|
|
105
|
+
ALTER TABLE conversation_turns ADD COLUMN plan_proposal_json TEXT;
|
|
106
|
+
END IF;
|
|
107
|
+
END $$;
|
|
108
|
+
`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function createThread(
|
|
112
|
+
db: PGlite,
|
|
113
|
+
input: {
|
|
114
|
+
id: string;
|
|
115
|
+
mode: ConversationMode;
|
|
116
|
+
harness: HarnessId;
|
|
117
|
+
modelLabel: string;
|
|
118
|
+
modelId: string;
|
|
119
|
+
sourceThreadId?: string | null;
|
|
120
|
+
taskId?: string | null;
|
|
121
|
+
},
|
|
122
|
+
): Promise<void> {
|
|
123
|
+
await db.query(
|
|
124
|
+
`
|
|
125
|
+
INSERT INTO conversation_threads (
|
|
126
|
+
id,
|
|
127
|
+
mode,
|
|
128
|
+
status,
|
|
129
|
+
harness,
|
|
130
|
+
model_label,
|
|
131
|
+
model_id,
|
|
132
|
+
source_thread_id,
|
|
133
|
+
task_id
|
|
134
|
+
)
|
|
135
|
+
VALUES ($1, $2, 'open', $3, $4, $5, $6, $7)
|
|
136
|
+
`,
|
|
137
|
+
[
|
|
138
|
+
input.id,
|
|
139
|
+
input.mode,
|
|
140
|
+
input.harness,
|
|
141
|
+
input.modelLabel,
|
|
142
|
+
input.modelId,
|
|
143
|
+
input.sourceThreadId ?? null,
|
|
144
|
+
input.taskId ?? null,
|
|
145
|
+
],
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function getThreadById(
|
|
150
|
+
db: PGlite,
|
|
151
|
+
threadId: string,
|
|
152
|
+
): Promise<ConversationThread | null> {
|
|
153
|
+
const result = await db.query<ConversationThread>(
|
|
154
|
+
`
|
|
155
|
+
SELECT id,
|
|
156
|
+
mode,
|
|
157
|
+
status,
|
|
158
|
+
harness,
|
|
159
|
+
model_label,
|
|
160
|
+
model_id,
|
|
161
|
+
harness_session_id,
|
|
162
|
+
task_id,
|
|
163
|
+
source_thread_id,
|
|
164
|
+
chat_title,
|
|
165
|
+
chat_preview,
|
|
166
|
+
chat_last_message_at,
|
|
167
|
+
created_at,
|
|
168
|
+
updated_at
|
|
169
|
+
FROM conversation_threads
|
|
170
|
+
WHERE id = $1
|
|
171
|
+
`,
|
|
172
|
+
[threadId],
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
return result.rows[0] ?? null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function listChatThreads(
|
|
179
|
+
db: PGlite,
|
|
180
|
+
input?: { limit?: number; cursor?: string | null },
|
|
181
|
+
): Promise<ChatThreadListItem[]> {
|
|
182
|
+
const requested = input?.limit ?? 20;
|
|
183
|
+
const limit = Math.max(1, Math.min(requested, 20));
|
|
184
|
+
const cursor = input?.cursor?.trim() || null;
|
|
185
|
+
|
|
186
|
+
const params: Array<string | number> = [limit];
|
|
187
|
+
let query = `
|
|
188
|
+
SELECT thread.id,
|
|
189
|
+
thread.chat_title,
|
|
190
|
+
thread.chat_preview,
|
|
191
|
+
thread.status,
|
|
192
|
+
thread.updated_at,
|
|
193
|
+
thread.chat_last_message_at,
|
|
194
|
+
newest_turn.status AS latest_turn_status
|
|
195
|
+
FROM conversation_threads AS thread
|
|
196
|
+
LEFT JOIN LATERAL (
|
|
197
|
+
SELECT status
|
|
198
|
+
FROM conversation_turns
|
|
199
|
+
WHERE thread_id = thread.id
|
|
200
|
+
ORDER BY created_at DESC
|
|
201
|
+
LIMIT 1
|
|
202
|
+
) AS newest_turn ON TRUE
|
|
203
|
+
WHERE thread.mode = 'chat'
|
|
204
|
+
`;
|
|
205
|
+
|
|
206
|
+
if (cursor) {
|
|
207
|
+
params.push(cursor);
|
|
208
|
+
query += " AND thread.created_at < $2::timestamptz\n";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
query += `
|
|
212
|
+
ORDER BY thread.created_at DESC
|
|
213
|
+
LIMIT $1
|
|
214
|
+
`;
|
|
215
|
+
|
|
216
|
+
const result = await db.query<ChatThreadListItem>(query, params);
|
|
217
|
+
return result.rows;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function listOpenPlanThreads(
|
|
221
|
+
db: PGlite,
|
|
222
|
+
input?: { limit?: number; cursor?: string | null },
|
|
223
|
+
): Promise<OpenPlanThreadListItem[]> {
|
|
224
|
+
const requested = input?.limit ?? 20;
|
|
225
|
+
const limit = Math.max(1, Math.min(requested, 20));
|
|
226
|
+
const cursor = input?.cursor?.trim() || null;
|
|
227
|
+
|
|
228
|
+
const params: Array<string | number> = [limit];
|
|
229
|
+
let query = `
|
|
230
|
+
SELECT thread.id,
|
|
231
|
+
thread.status,
|
|
232
|
+
thread.created_at,
|
|
233
|
+
thread.updated_at,
|
|
234
|
+
thread.task_id,
|
|
235
|
+
latest_plan_turn.assistant_message AS latest_plan_assistant_message,
|
|
236
|
+
first_plan_turn.user_message AS first_user_message,
|
|
237
|
+
(latest_plan_turn.assistant_message IS NOT NULL) AS has_completed_plan_turn,
|
|
238
|
+
newest_turn.status AS latest_turn_status,
|
|
239
|
+
task.status AS task_status
|
|
240
|
+
FROM conversation_threads AS thread
|
|
241
|
+
LEFT JOIN LATERAL (
|
|
242
|
+
SELECT assistant_message
|
|
243
|
+
FROM conversation_turns
|
|
244
|
+
WHERE thread_id = thread.id
|
|
245
|
+
AND mode = 'plan'
|
|
246
|
+
AND status = 'completed'
|
|
247
|
+
AND assistant_message IS NOT NULL
|
|
248
|
+
AND assistant_message <> ''
|
|
249
|
+
ORDER BY created_at DESC
|
|
250
|
+
LIMIT 1
|
|
251
|
+
) AS latest_plan_turn ON TRUE
|
|
252
|
+
LEFT JOIN LATERAL (
|
|
253
|
+
SELECT user_message
|
|
254
|
+
FROM conversation_turns
|
|
255
|
+
WHERE thread_id = thread.id
|
|
256
|
+
AND mode = 'plan'
|
|
257
|
+
ORDER BY created_at ASC
|
|
258
|
+
LIMIT 1
|
|
259
|
+
) AS first_plan_turn ON TRUE
|
|
260
|
+
LEFT JOIN LATERAL (
|
|
261
|
+
SELECT status
|
|
262
|
+
FROM conversation_turns
|
|
263
|
+
WHERE thread_id = thread.id
|
|
264
|
+
AND mode = 'plan'
|
|
265
|
+
ORDER BY created_at DESC
|
|
266
|
+
LIMIT 1
|
|
267
|
+
) AS newest_turn ON TRUE
|
|
268
|
+
LEFT JOIN tasks AS task ON task.id = thread.task_id
|
|
269
|
+
WHERE thread.mode = 'plan'
|
|
270
|
+
AND thread.status = 'open'
|
|
271
|
+
`;
|
|
272
|
+
|
|
273
|
+
if (cursor) {
|
|
274
|
+
params.push(cursor);
|
|
275
|
+
query += " AND thread.updated_at < $2::timestamptz\n";
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
query += `
|
|
279
|
+
ORDER BY thread.updated_at DESC
|
|
280
|
+
LIMIT $1
|
|
281
|
+
`;
|
|
282
|
+
|
|
283
|
+
const result = await db.query<OpenPlanThreadListItem>(query, params);
|
|
284
|
+
return result.rows;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export async function updateChatThreadMetadata(
|
|
288
|
+
db: PGlite,
|
|
289
|
+
input: {
|
|
290
|
+
threadId: string;
|
|
291
|
+
title?: string | null;
|
|
292
|
+
lastMessageAt?: string | null;
|
|
293
|
+
force?: boolean;
|
|
294
|
+
},
|
|
295
|
+
): Promise<void> {
|
|
296
|
+
const titleSql = input.force
|
|
297
|
+
? `chat_title = CASE
|
|
298
|
+
WHEN $2::text IS NOT NULL AND $2::text <> ''
|
|
299
|
+
THEN $2::text
|
|
300
|
+
ELSE chat_title
|
|
301
|
+
END`
|
|
302
|
+
: `chat_title = CASE
|
|
303
|
+
WHEN (chat_title IS NULL OR chat_title = '') AND $2::text IS NOT NULL AND $2::text <> ''
|
|
304
|
+
THEN $2::text
|
|
305
|
+
ELSE chat_title
|
|
306
|
+
END`;
|
|
307
|
+
|
|
308
|
+
await db.query(
|
|
309
|
+
`
|
|
310
|
+
UPDATE conversation_threads
|
|
311
|
+
SET ${titleSql},
|
|
312
|
+
chat_last_message_at = COALESCE($3::timestamptz, CURRENT_TIMESTAMP),
|
|
313
|
+
updated_at = CURRENT_TIMESTAMP
|
|
314
|
+
WHERE id = $1
|
|
315
|
+
`,
|
|
316
|
+
[input.threadId, input.title ?? null, input.lastMessageAt ?? null],
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export async function updateThreadSession(
|
|
321
|
+
db: PGlite,
|
|
322
|
+
input: { threadId: string; sessionId: string },
|
|
323
|
+
): Promise<void> {
|
|
324
|
+
await db.query(
|
|
325
|
+
`
|
|
326
|
+
UPDATE conversation_threads
|
|
327
|
+
SET harness_session_id = $2,
|
|
328
|
+
updated_at = CURRENT_TIMESTAMP
|
|
329
|
+
WHERE id = $1
|
|
330
|
+
`,
|
|
331
|
+
[input.threadId, input.sessionId],
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export async function closeThread(db: PGlite, threadId: string): Promise<void> {
|
|
336
|
+
await db.query(
|
|
337
|
+
`
|
|
338
|
+
UPDATE conversation_threads
|
|
339
|
+
SET status = 'closed',
|
|
340
|
+
updated_at = CURRENT_TIMESTAMP
|
|
341
|
+
WHERE id = $1
|
|
342
|
+
`,
|
|
343
|
+
[threadId],
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export async function reopenThread(db: PGlite, threadId: string): Promise<void> {
|
|
348
|
+
await db.query(
|
|
349
|
+
`
|
|
350
|
+
UPDATE conversation_threads
|
|
351
|
+
SET status = 'open',
|
|
352
|
+
updated_at = CURRENT_TIMESTAMP
|
|
353
|
+
WHERE id = $1
|
|
354
|
+
`,
|
|
355
|
+
[threadId],
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export async function createTurn(
|
|
360
|
+
db: PGlite,
|
|
361
|
+
input: {
|
|
362
|
+
id: string;
|
|
363
|
+
threadId: string;
|
|
364
|
+
mode: ConversationMode;
|
|
365
|
+
userMessage: string;
|
|
366
|
+
reasoningEffort?: ReasoningEffort | null;
|
|
367
|
+
requestedRecipientAgentId?: string | null;
|
|
368
|
+
selectedAgentId?: string | null;
|
|
369
|
+
orchestratorAgentId?: string | null;
|
|
370
|
+
workerSessionId?: string | null;
|
|
371
|
+
selectionStatus?: "auto_selected" | "manual_selected" | "recipient_required" | "invalid" | null;
|
|
372
|
+
},
|
|
373
|
+
): Promise<void> {
|
|
374
|
+
await db.query(
|
|
375
|
+
`
|
|
376
|
+
INSERT INTO conversation_turns (
|
|
377
|
+
id,
|
|
378
|
+
thread_id,
|
|
379
|
+
mode,
|
|
380
|
+
user_message,
|
|
381
|
+
reasoning_effort,
|
|
382
|
+
requested_recipient_agent_id,
|
|
383
|
+
selected_agent_id,
|
|
384
|
+
orchestrator_agent_id,
|
|
385
|
+
worker_session_id,
|
|
386
|
+
selection_status,
|
|
387
|
+
status
|
|
388
|
+
)
|
|
389
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'running')
|
|
390
|
+
`,
|
|
391
|
+
[
|
|
392
|
+
input.id,
|
|
393
|
+
input.threadId,
|
|
394
|
+
input.mode,
|
|
395
|
+
input.userMessage,
|
|
396
|
+
input.reasoningEffort ?? null,
|
|
397
|
+
input.requestedRecipientAgentId ?? null,
|
|
398
|
+
input.selectedAgentId ?? null,
|
|
399
|
+
input.orchestratorAgentId ?? null,
|
|
400
|
+
input.workerSessionId ?? null,
|
|
401
|
+
input.selectionStatus ?? null,
|
|
402
|
+
],
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export async function completeTurn(
|
|
407
|
+
db: PGlite,
|
|
408
|
+
input: {
|
|
409
|
+
turnId: string;
|
|
410
|
+
assistantMessage: string;
|
|
411
|
+
selectedAgentId?: string | null;
|
|
412
|
+
workerSessionId?: string | null;
|
|
413
|
+
selectionStatus?: "auto_selected" | "manual_selected" | "recipient_required" | "invalid" | null;
|
|
414
|
+
},
|
|
415
|
+
): Promise<void> {
|
|
416
|
+
await db.query(
|
|
417
|
+
`
|
|
418
|
+
UPDATE conversation_turns
|
|
419
|
+
SET assistant_message = $2,
|
|
420
|
+
selected_agent_id = COALESCE($3, selected_agent_id),
|
|
421
|
+
worker_session_id = COALESCE($4, worker_session_id),
|
|
422
|
+
selection_status = COALESCE($5, selection_status),
|
|
423
|
+
status = 'completed',
|
|
424
|
+
completed_at = CURRENT_TIMESTAMP
|
|
425
|
+
WHERE id = $1
|
|
426
|
+
`,
|
|
427
|
+
[
|
|
428
|
+
input.turnId,
|
|
429
|
+
input.assistantMessage,
|
|
430
|
+
input.selectedAgentId ?? null,
|
|
431
|
+
input.workerSessionId ?? null,
|
|
432
|
+
input.selectionStatus ?? null,
|
|
433
|
+
],
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export async function failTurn(db: PGlite, turnId: string, message: string): Promise<void> {
|
|
438
|
+
await db.query(
|
|
439
|
+
`
|
|
440
|
+
UPDATE conversation_turns
|
|
441
|
+
SET assistant_message = COALESCE(assistant_message, $2),
|
|
442
|
+
status = 'failed',
|
|
443
|
+
completed_at = CURRENT_TIMESTAMP
|
|
444
|
+
WHERE id = $1
|
|
445
|
+
`,
|
|
446
|
+
[turnId, message],
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export async function insertMessage(
|
|
451
|
+
db: PGlite,
|
|
452
|
+
input: {
|
|
453
|
+
id: string;
|
|
454
|
+
threadId: string;
|
|
455
|
+
turnId: string | null;
|
|
456
|
+
role: "user" | "assistant" | "system";
|
|
457
|
+
content: string;
|
|
458
|
+
},
|
|
459
|
+
): Promise<void> {
|
|
460
|
+
await db.query(
|
|
461
|
+
`
|
|
462
|
+
INSERT INTO conversation_messages (id, thread_id, turn_id, role, content)
|
|
463
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
464
|
+
`,
|
|
465
|
+
[input.id, input.threadId, input.turnId, input.role, input.content],
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export async function insertEvent(
|
|
470
|
+
db: PGlite,
|
|
471
|
+
input: {
|
|
472
|
+
id: string;
|
|
473
|
+
threadId: string;
|
|
474
|
+
turnId: string | null;
|
|
475
|
+
eventName: string;
|
|
476
|
+
payload: unknown;
|
|
477
|
+
},
|
|
478
|
+
): Promise<number> {
|
|
479
|
+
const result = await db.query<{ seq: number }>(
|
|
480
|
+
`
|
|
481
|
+
INSERT INTO conversation_events (id, thread_id, turn_id, event_name, payload_json)
|
|
482
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
483
|
+
RETURNING seq
|
|
484
|
+
`,
|
|
485
|
+
[input.id, input.threadId, input.turnId, input.eventName, JSON.stringify(input.payload ?? null)],
|
|
486
|
+
);
|
|
487
|
+
return result.rows[0]?.seq ?? 0;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export async function getEventsSince(
|
|
491
|
+
db: PGlite,
|
|
492
|
+
threadId: string,
|
|
493
|
+
afterSeq: number,
|
|
494
|
+
): Promise<Array<ConversationEvent & { seq: number }>> {
|
|
495
|
+
const result = await db.query<ConversationEvent & { seq: number }>(
|
|
496
|
+
`
|
|
497
|
+
SELECT id, seq, thread_id, turn_id, event_name, payload_json, created_at
|
|
498
|
+
FROM conversation_events
|
|
499
|
+
WHERE thread_id = $1
|
|
500
|
+
AND seq > $2
|
|
501
|
+
ORDER BY seq ASC
|
|
502
|
+
`,
|
|
503
|
+
[threadId, afterSeq],
|
|
504
|
+
);
|
|
505
|
+
return result.rows;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export async function getMaxEventSeq(
|
|
509
|
+
db: PGlite,
|
|
510
|
+
threadId: string,
|
|
511
|
+
): Promise<number> {
|
|
512
|
+
const result = await db.query<{ max_seq: number | null }>(
|
|
513
|
+
`SELECT MAX(seq) AS max_seq FROM conversation_events WHERE thread_id = $1`,
|
|
514
|
+
[threadId],
|
|
515
|
+
);
|
|
516
|
+
return result.rows[0]?.max_seq ?? 0;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export async function setThreadTaskId(db: PGlite, threadId: string, taskId: string): Promise<void> {
|
|
520
|
+
await db.query(
|
|
521
|
+
`
|
|
522
|
+
UPDATE conversation_threads
|
|
523
|
+
SET task_id = $2,
|
|
524
|
+
updated_at = CURRENT_TIMESTAMP
|
|
525
|
+
WHERE id = $1
|
|
526
|
+
`,
|
|
527
|
+
[threadId, taskId],
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export async function getThreadByTaskId(
|
|
532
|
+
db: PGlite,
|
|
533
|
+
taskId: string,
|
|
534
|
+
): Promise<ConversationThread | null> {
|
|
535
|
+
const result = await db.query<ConversationThread>(
|
|
536
|
+
`
|
|
537
|
+
SELECT id,
|
|
538
|
+
mode,
|
|
539
|
+
status,
|
|
540
|
+
harness,
|
|
541
|
+
model_label,
|
|
542
|
+
model_id,
|
|
543
|
+
harness_session_id,
|
|
544
|
+
task_id,
|
|
545
|
+
source_thread_id,
|
|
546
|
+
chat_title,
|
|
547
|
+
chat_preview,
|
|
548
|
+
chat_last_message_at,
|
|
549
|
+
created_at,
|
|
550
|
+
updated_at
|
|
551
|
+
FROM conversation_threads
|
|
552
|
+
WHERE task_id = $1
|
|
553
|
+
ORDER BY created_at DESC
|
|
554
|
+
LIMIT 1
|
|
555
|
+
`,
|
|
556
|
+
[taskId],
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
return result.rows[0] ?? null;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export async function getLatestPlanTurn(
|
|
563
|
+
db: PGlite,
|
|
564
|
+
threadId: string,
|
|
565
|
+
): Promise<ConversationTurn | null> {
|
|
566
|
+
const result = await db.query<ConversationTurn>(
|
|
567
|
+
`
|
|
568
|
+
SELECT id,
|
|
569
|
+
thread_id,
|
|
570
|
+
mode,
|
|
571
|
+
user_message,
|
|
572
|
+
assistant_message,
|
|
573
|
+
reasoning_effort,
|
|
574
|
+
requested_recipient_agent_id,
|
|
575
|
+
selected_agent_id,
|
|
576
|
+
orchestrator_agent_id,
|
|
577
|
+
worker_session_id,
|
|
578
|
+
selection_status,
|
|
579
|
+
status,
|
|
580
|
+
created_at,
|
|
581
|
+
completed_at
|
|
582
|
+
FROM conversation_turns
|
|
583
|
+
WHERE thread_id = $1
|
|
584
|
+
AND mode = 'plan'
|
|
585
|
+
ORDER BY created_at DESC
|
|
586
|
+
LIMIT 1
|
|
587
|
+
`,
|
|
588
|
+
[threadId],
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
return result.rows[0] ?? null;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export async function getLatestCompletedPlanTurn(
|
|
595
|
+
db: PGlite,
|
|
596
|
+
threadId: string,
|
|
597
|
+
): Promise<ConversationTurn | null> {
|
|
598
|
+
const result = await db.query<ConversationTurn>(
|
|
599
|
+
`
|
|
600
|
+
SELECT id,
|
|
601
|
+
thread_id,
|
|
602
|
+
mode,
|
|
603
|
+
user_message,
|
|
604
|
+
assistant_message,
|
|
605
|
+
reasoning_effort,
|
|
606
|
+
requested_recipient_agent_id,
|
|
607
|
+
selected_agent_id,
|
|
608
|
+
orchestrator_agent_id,
|
|
609
|
+
worker_session_id,
|
|
610
|
+
selection_status,
|
|
611
|
+
status,
|
|
612
|
+
created_at,
|
|
613
|
+
completed_at
|
|
614
|
+
FROM conversation_turns
|
|
615
|
+
WHERE thread_id = $1
|
|
616
|
+
AND mode = 'plan'
|
|
617
|
+
AND status = 'completed'
|
|
618
|
+
ORDER BY created_at DESC
|
|
619
|
+
LIMIT 1
|
|
620
|
+
`,
|
|
621
|
+
[threadId],
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
return result.rows[0] ?? null;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export async function getLatestExecuteTurn(
|
|
628
|
+
db: PGlite,
|
|
629
|
+
threadId: string,
|
|
630
|
+
): Promise<ConversationTurn | null> {
|
|
631
|
+
const result = await db.query<ConversationTurn>(
|
|
632
|
+
`
|
|
633
|
+
SELECT id,
|
|
634
|
+
thread_id,
|
|
635
|
+
mode,
|
|
636
|
+
user_message,
|
|
637
|
+
assistant_message,
|
|
638
|
+
reasoning_effort,
|
|
639
|
+
requested_recipient_agent_id,
|
|
640
|
+
selected_agent_id,
|
|
641
|
+
orchestrator_agent_id,
|
|
642
|
+
worker_session_id,
|
|
643
|
+
selection_status,
|
|
644
|
+
status,
|
|
645
|
+
created_at,
|
|
646
|
+
completed_at
|
|
647
|
+
FROM conversation_turns
|
|
648
|
+
WHERE thread_id = $1
|
|
649
|
+
AND mode = 'execute'
|
|
650
|
+
ORDER BY created_at DESC
|
|
651
|
+
LIMIT 1
|
|
652
|
+
`,
|
|
653
|
+
[threadId],
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
return result.rows[0] ?? null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export async function getFirstExecuteTurn(
|
|
660
|
+
db: PGlite,
|
|
661
|
+
threadId: string,
|
|
662
|
+
): Promise<ConversationTurn | null> {
|
|
663
|
+
const result = await db.query<ConversationTurn>(
|
|
664
|
+
`
|
|
665
|
+
SELECT id,
|
|
666
|
+
thread_id,
|
|
667
|
+
mode,
|
|
668
|
+
user_message,
|
|
669
|
+
assistant_message,
|
|
670
|
+
reasoning_effort,
|
|
671
|
+
requested_recipient_agent_id,
|
|
672
|
+
selected_agent_id,
|
|
673
|
+
orchestrator_agent_id,
|
|
674
|
+
worker_session_id,
|
|
675
|
+
selection_status,
|
|
676
|
+
status,
|
|
677
|
+
created_at,
|
|
678
|
+
completed_at
|
|
679
|
+
FROM conversation_turns
|
|
680
|
+
WHERE thread_id = $1
|
|
681
|
+
AND mode = 'execute'
|
|
682
|
+
ORDER BY created_at ASC
|
|
683
|
+
LIMIT 1
|
|
684
|
+
`,
|
|
685
|
+
[threadId],
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
return result.rows[0] ?? null;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export async function getThreadWithDetails(
|
|
692
|
+
db: PGlite,
|
|
693
|
+
threadId: string,
|
|
694
|
+
): Promise<{
|
|
695
|
+
thread: ConversationThread | null;
|
|
696
|
+
turns: ConversationTurn[];
|
|
697
|
+
messages: ConversationMessage[];
|
|
698
|
+
events: ConversationEvent[];
|
|
699
|
+
}> {
|
|
700
|
+
const thread = await getThreadById(db, threadId);
|
|
701
|
+
|
|
702
|
+
const [turnsResult, messagesResult, eventsResult] = await Promise.all([
|
|
703
|
+
db.query<ConversationTurn>(
|
|
704
|
+
`
|
|
705
|
+
SELECT id,
|
|
706
|
+
thread_id,
|
|
707
|
+
mode,
|
|
708
|
+
user_message,
|
|
709
|
+
assistant_message,
|
|
710
|
+
reasoning_effort,
|
|
711
|
+
requested_recipient_agent_id,
|
|
712
|
+
selected_agent_id,
|
|
713
|
+
orchestrator_agent_id,
|
|
714
|
+
worker_session_id,
|
|
715
|
+
selection_status,
|
|
716
|
+
status,
|
|
717
|
+
created_at,
|
|
718
|
+
completed_at
|
|
719
|
+
FROM conversation_turns
|
|
720
|
+
WHERE thread_id = $1
|
|
721
|
+
ORDER BY created_at ASC
|
|
722
|
+
`,
|
|
723
|
+
[threadId],
|
|
724
|
+
),
|
|
725
|
+
db.query<ConversationMessage>(
|
|
726
|
+
`
|
|
727
|
+
SELECT id, thread_id, turn_id, role, content, created_at
|
|
728
|
+
FROM conversation_messages
|
|
729
|
+
WHERE thread_id = $1
|
|
730
|
+
ORDER BY created_at ASC
|
|
731
|
+
`,
|
|
732
|
+
[threadId],
|
|
733
|
+
),
|
|
734
|
+
db.query<ConversationEvent & { seq: number }>(
|
|
735
|
+
`
|
|
736
|
+
SELECT id, seq, thread_id, turn_id, event_name, payload_json, created_at
|
|
737
|
+
FROM conversation_events
|
|
738
|
+
WHERE thread_id = $1
|
|
739
|
+
ORDER BY seq ASC
|
|
740
|
+
`,
|
|
741
|
+
[threadId],
|
|
742
|
+
),
|
|
743
|
+
]);
|
|
744
|
+
|
|
745
|
+
return {
|
|
746
|
+
thread,
|
|
747
|
+
turns: turnsResult.rows,
|
|
748
|
+
messages: messagesResult.rows,
|
|
749
|
+
events: eventsResult.rows,
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export async function getThreadMessages(
|
|
754
|
+
db: PGlite,
|
|
755
|
+
threadId: string,
|
|
756
|
+
limit = 40,
|
|
757
|
+
): Promise<Array<{ role: string; content: string; created_at: string }>> {
|
|
758
|
+
const result = await db.query<{ role: string; content: string; created_at: string }>(
|
|
759
|
+
`SELECT role, content, created_at
|
|
760
|
+
FROM conversation_messages
|
|
761
|
+
WHERE thread_id = $1
|
|
762
|
+
ORDER BY created_at ASC
|
|
763
|
+
LIMIT $2`,
|
|
764
|
+
[threadId, limit],
|
|
765
|
+
);
|
|
766
|
+
return result.rows;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
export async function storePlanProposal(
|
|
770
|
+
db: PGlite,
|
|
771
|
+
turnId: string,
|
|
772
|
+
proposal: unknown,
|
|
773
|
+
): Promise<void> {
|
|
774
|
+
await db.query(
|
|
775
|
+
`UPDATE conversation_turns SET plan_proposal_json = $2 WHERE id = $1`,
|
|
776
|
+
[turnId, JSON.stringify(proposal)],
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
export async function getPlanProposal(
|
|
781
|
+
db: PGlite,
|
|
782
|
+
threadId: string,
|
|
783
|
+
): Promise<unknown | null> {
|
|
784
|
+
const result = await db.query<{ plan_proposal_json: string | null }>(
|
|
785
|
+
`
|
|
786
|
+
SELECT plan_proposal_json
|
|
787
|
+
FROM conversation_turns
|
|
788
|
+
WHERE thread_id = $1
|
|
789
|
+
AND mode = 'plan'
|
|
790
|
+
AND plan_proposal_json IS NOT NULL
|
|
791
|
+
ORDER BY created_at DESC
|
|
792
|
+
LIMIT 1
|
|
793
|
+
`,
|
|
794
|
+
[threadId],
|
|
795
|
+
);
|
|
796
|
+
const row = result.rows[0];
|
|
797
|
+
if (!row?.plan_proposal_json) {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
try {
|
|
801
|
+
return JSON.parse(row.plan_proposal_json);
|
|
802
|
+
} catch {
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
}
|