@agi-cli/server 0.1.119 → 0.1.120
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/package.json +3 -3
- package/src/index.ts +4 -0
- package/src/routes/branch.ts +106 -0
- package/src/runtime/branch.ts +277 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agi-cli/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.120",
|
|
4
4
|
"description": "HTTP API server for AGI CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"typecheck": "tsc --noEmit"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@agi-cli/sdk": "0.1.
|
|
33
|
-
"@agi-cli/database": "0.1.
|
|
32
|
+
"@agi-cli/sdk": "0.1.120",
|
|
33
|
+
"@agi-cli/database": "0.1.120",
|
|
34
34
|
"drizzle-orm": "^0.44.5",
|
|
35
35
|
"hono": "^4.9.9",
|
|
36
36
|
"zod": "^4.1.8"
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { registerFilesRoutes } from './routes/files.ts';
|
|
|
14
14
|
import { registerGitRoutes } from './routes/git/index.ts';
|
|
15
15
|
import { registerTerminalsRoutes } from './routes/terminals.ts';
|
|
16
16
|
import { registerSessionFilesRoutes } from './routes/session-files.ts';
|
|
17
|
+
import { registerBranchRoutes } from './routes/branch.ts';
|
|
17
18
|
import type { AgentConfigEntry } from './runtime/agent-registry.ts';
|
|
18
19
|
|
|
19
20
|
const globalTerminalManager = new TerminalManager();
|
|
@@ -64,6 +65,7 @@ function initApp() {
|
|
|
64
65
|
registerGitRoutes(app);
|
|
65
66
|
registerTerminalsRoutes(app, globalTerminalManager);
|
|
66
67
|
registerSessionFilesRoutes(app);
|
|
68
|
+
registerBranchRoutes(app);
|
|
67
69
|
|
|
68
70
|
return app;
|
|
69
71
|
}
|
|
@@ -130,6 +132,7 @@ export function createStandaloneApp(_config?: StandaloneAppConfig) {
|
|
|
130
132
|
registerGitRoutes(honoApp);
|
|
131
133
|
registerTerminalsRoutes(honoApp, globalTerminalManager);
|
|
132
134
|
registerSessionFilesRoutes(honoApp);
|
|
135
|
+
registerBranchRoutes(honoApp);
|
|
133
136
|
|
|
134
137
|
return honoApp;
|
|
135
138
|
}
|
|
@@ -224,6 +227,7 @@ export function createEmbeddedApp(config: EmbeddedAppConfig = {}) {
|
|
|
224
227
|
registerGitRoutes(honoApp);
|
|
225
228
|
registerTerminalsRoutes(honoApp, globalTerminalManager);
|
|
226
229
|
registerSessionFilesRoutes(honoApp);
|
|
230
|
+
registerBranchRoutes(honoApp);
|
|
227
231
|
|
|
228
232
|
return honoApp;
|
|
229
233
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import { loadConfig } from '@agi-cli/sdk';
|
|
3
|
+
import { getDb } from '@agi-cli/database';
|
|
4
|
+
import { isProviderId, logger } from '@agi-cli/sdk';
|
|
5
|
+
import {
|
|
6
|
+
createBranch,
|
|
7
|
+
listBranches,
|
|
8
|
+
getParentSession,
|
|
9
|
+
} from '../runtime/branch.ts';
|
|
10
|
+
import { serializeError } from '../runtime/api-error.ts';
|
|
11
|
+
|
|
12
|
+
export function registerBranchRoutes(app: Hono) {
|
|
13
|
+
app.post('/v1/sessions/:sessionId/branch', async (c) => {
|
|
14
|
+
try {
|
|
15
|
+
const sessionId = c.req.param('sessionId');
|
|
16
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
17
|
+
const cfg = await loadConfig(projectRoot);
|
|
18
|
+
const db = await getDb(cfg.projectRoot);
|
|
19
|
+
|
|
20
|
+
const body = (await c.req.json().catch(() => ({}))) as Record<
|
|
21
|
+
string,
|
|
22
|
+
unknown
|
|
23
|
+
>;
|
|
24
|
+
|
|
25
|
+
const fromMessageId = body.fromMessageId;
|
|
26
|
+
if (typeof fromMessageId !== 'string' || !fromMessageId.trim()) {
|
|
27
|
+
return c.json({ error: 'fromMessageId is required' }, 400);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const provider =
|
|
31
|
+
typeof body.provider === 'string' && isProviderId(body.provider)
|
|
32
|
+
? body.provider
|
|
33
|
+
: undefined;
|
|
34
|
+
|
|
35
|
+
const model =
|
|
36
|
+
typeof body.model === 'string' && body.model.trim()
|
|
37
|
+
? body.model.trim()
|
|
38
|
+
: undefined;
|
|
39
|
+
|
|
40
|
+
const agent =
|
|
41
|
+
typeof body.agent === 'string' && body.agent.trim()
|
|
42
|
+
? body.agent.trim()
|
|
43
|
+
: undefined;
|
|
44
|
+
|
|
45
|
+
const title =
|
|
46
|
+
typeof body.title === 'string' && body.title.trim()
|
|
47
|
+
? body.title.trim()
|
|
48
|
+
: undefined;
|
|
49
|
+
|
|
50
|
+
const result = await createBranch({
|
|
51
|
+
db,
|
|
52
|
+
parentSessionId: sessionId,
|
|
53
|
+
fromMessageId: fromMessageId.trim(),
|
|
54
|
+
provider,
|
|
55
|
+
model,
|
|
56
|
+
agent,
|
|
57
|
+
title,
|
|
58
|
+
projectPath: cfg.projectRoot,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return c.json(result, 201);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
logger.error('Failed to create branch', err);
|
|
64
|
+
const errorResponse = serializeError(err);
|
|
65
|
+
return c.json(errorResponse, errorResponse.error.status || 400);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
app.get('/v1/sessions/:sessionId/branches', async (c) => {
|
|
70
|
+
try {
|
|
71
|
+
const sessionId = c.req.param('sessionId');
|
|
72
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
73
|
+
const cfg = await loadConfig(projectRoot);
|
|
74
|
+
const db = await getDb(cfg.projectRoot);
|
|
75
|
+
|
|
76
|
+
const branches = await listBranches(db, sessionId, cfg.projectRoot);
|
|
77
|
+
|
|
78
|
+
return c.json({ branches });
|
|
79
|
+
} catch (err) {
|
|
80
|
+
logger.error('Failed to list branches', err);
|
|
81
|
+
const errorResponse = serializeError(err);
|
|
82
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
app.get('/v1/sessions/:sessionId/parent', async (c) => {
|
|
87
|
+
try {
|
|
88
|
+
const sessionId = c.req.param('sessionId');
|
|
89
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
90
|
+
const cfg = await loadConfig(projectRoot);
|
|
91
|
+
const db = await getDb(cfg.projectRoot);
|
|
92
|
+
|
|
93
|
+
const parent = await getParentSession(db, sessionId, cfg.projectRoot);
|
|
94
|
+
|
|
95
|
+
if (!parent) {
|
|
96
|
+
return c.json({ parent: null });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return c.json({ parent });
|
|
100
|
+
} catch (err) {
|
|
101
|
+
logger.error('Failed to get parent session', err);
|
|
102
|
+
const errorResponse = serializeError(err);
|
|
103
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { eq, asc } from 'drizzle-orm';
|
|
2
|
+
import type { DB } from '@agi-cli/database';
|
|
3
|
+
import { sessions, messages, messageParts } from '@agi-cli/database/schema';
|
|
4
|
+
import { publish } from '../events/bus.ts';
|
|
5
|
+
import type { ProviderId } from '@agi-cli/sdk';
|
|
6
|
+
|
|
7
|
+
type SessionRow = typeof sessions.$inferSelect;
|
|
8
|
+
|
|
9
|
+
export type CreateBranchInput = {
|
|
10
|
+
db: DB;
|
|
11
|
+
parentSessionId: string;
|
|
12
|
+
fromMessageId: string;
|
|
13
|
+
provider?: ProviderId;
|
|
14
|
+
model?: string;
|
|
15
|
+
agent?: string;
|
|
16
|
+
title?: string;
|
|
17
|
+
projectPath: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type BranchResult = {
|
|
21
|
+
session: SessionRow;
|
|
22
|
+
parentSessionId: string;
|
|
23
|
+
branchPointMessageId: string;
|
|
24
|
+
copiedMessages: number;
|
|
25
|
+
copiedParts: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export async function createBranch({
|
|
29
|
+
db,
|
|
30
|
+
parentSessionId,
|
|
31
|
+
fromMessageId,
|
|
32
|
+
provider,
|
|
33
|
+
model,
|
|
34
|
+
agent,
|
|
35
|
+
title,
|
|
36
|
+
projectPath,
|
|
37
|
+
}: CreateBranchInput): Promise<BranchResult> {
|
|
38
|
+
const parentRows = await db
|
|
39
|
+
.select()
|
|
40
|
+
.from(sessions)
|
|
41
|
+
.where(eq(sessions.id, parentSessionId));
|
|
42
|
+
|
|
43
|
+
if (!parentRows.length) {
|
|
44
|
+
throw new Error('Parent session not found');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const parent = parentRows[0];
|
|
48
|
+
|
|
49
|
+
if (parent.projectPath !== projectPath) {
|
|
50
|
+
throw new Error('Parent session not found in this project');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const branchPointRows = await db
|
|
54
|
+
.select()
|
|
55
|
+
.from(messages)
|
|
56
|
+
.where(eq(messages.id, fromMessageId));
|
|
57
|
+
|
|
58
|
+
if (!branchPointRows.length) {
|
|
59
|
+
throw new Error('Branch point message not found');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const branchPoint = branchPointRows[0];
|
|
63
|
+
|
|
64
|
+
if (branchPoint.sessionId !== parentSessionId) {
|
|
65
|
+
throw new Error('Branch point message does not belong to parent session');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const allMessages = await db
|
|
69
|
+
.select()
|
|
70
|
+
.from(messages)
|
|
71
|
+
.where(eq(messages.sessionId, parentSessionId))
|
|
72
|
+
.orderBy(asc(messages.createdAt));
|
|
73
|
+
|
|
74
|
+
const branchPointIndex = allMessages.findIndex((m) => m.id === fromMessageId);
|
|
75
|
+
if (branchPointIndex === -1) {
|
|
76
|
+
throw new Error('Branch point message not found in session');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const messagesToCopy = allMessages.slice(0, branchPointIndex + 1);
|
|
80
|
+
|
|
81
|
+
const newSessionId = crypto.randomUUID();
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
|
|
84
|
+
const newSession: typeof sessions.$inferInsert = {
|
|
85
|
+
id: newSessionId,
|
|
86
|
+
title: title || `Branch of ${parent.title || 'Untitled'}`,
|
|
87
|
+
agent: agent || parent.agent,
|
|
88
|
+
provider: provider || parent.provider,
|
|
89
|
+
model: model || parent.model,
|
|
90
|
+
projectPath: parent.projectPath,
|
|
91
|
+
createdAt: now,
|
|
92
|
+
lastActiveAt: now,
|
|
93
|
+
parentSessionId,
|
|
94
|
+
branchPointMessageId: fromMessageId,
|
|
95
|
+
sessionType: 'branch',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
await db.insert(sessions).values(newSession);
|
|
99
|
+
|
|
100
|
+
const messageIdMap = new Map<string, string>();
|
|
101
|
+
let copiedParts = 0;
|
|
102
|
+
|
|
103
|
+
for (const msg of messagesToCopy) {
|
|
104
|
+
const newMessageId = crypto.randomUUID();
|
|
105
|
+
messageIdMap.set(msg.id, newMessageId);
|
|
106
|
+
|
|
107
|
+
const newMessage: typeof messages.$inferInsert = {
|
|
108
|
+
id: newMessageId,
|
|
109
|
+
sessionId: newSessionId,
|
|
110
|
+
role: msg.role,
|
|
111
|
+
status: msg.status,
|
|
112
|
+
agent: msg.agent,
|
|
113
|
+
provider: msg.provider,
|
|
114
|
+
model: msg.model,
|
|
115
|
+
createdAt: msg.createdAt,
|
|
116
|
+
completedAt: msg.completedAt,
|
|
117
|
+
latencyMs: msg.latencyMs,
|
|
118
|
+
promptTokens: msg.promptTokens,
|
|
119
|
+
completionTokens: msg.completionTokens,
|
|
120
|
+
totalTokens: msg.totalTokens,
|
|
121
|
+
cachedInputTokens: msg.cachedInputTokens,
|
|
122
|
+
reasoningTokens: msg.reasoningTokens,
|
|
123
|
+
error: msg.error,
|
|
124
|
+
errorType: msg.errorType,
|
|
125
|
+
errorDetails: msg.errorDetails,
|
|
126
|
+
isAborted: msg.isAborted,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
await db.insert(messages).values(newMessage);
|
|
130
|
+
|
|
131
|
+
const parts = await db
|
|
132
|
+
.select()
|
|
133
|
+
.from(messageParts)
|
|
134
|
+
.where(eq(messageParts.messageId, msg.id))
|
|
135
|
+
.orderBy(asc(messageParts.index));
|
|
136
|
+
|
|
137
|
+
for (const part of parts) {
|
|
138
|
+
const newPart: typeof messageParts.$inferInsert = {
|
|
139
|
+
id: crypto.randomUUID(),
|
|
140
|
+
messageId: newMessageId,
|
|
141
|
+
index: part.index,
|
|
142
|
+
stepIndex: part.stepIndex,
|
|
143
|
+
type: part.type,
|
|
144
|
+
content: part.content,
|
|
145
|
+
agent: part.agent,
|
|
146
|
+
provider: part.provider,
|
|
147
|
+
model: part.model,
|
|
148
|
+
startedAt: part.startedAt,
|
|
149
|
+
completedAt: part.completedAt,
|
|
150
|
+
compactedAt: part.compactedAt,
|
|
151
|
+
toolName: part.toolName,
|
|
152
|
+
toolCallId: part.toolCallId,
|
|
153
|
+
toolDurationMs: part.toolDurationMs,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
await db.insert(messageParts).values(newPart);
|
|
157
|
+
copiedParts++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const result: SessionRow = {
|
|
162
|
+
...newSession,
|
|
163
|
+
totalInputTokens: null,
|
|
164
|
+
totalOutputTokens: null,
|
|
165
|
+
totalCachedTokens: null,
|
|
166
|
+
totalReasoningTokens: null,
|
|
167
|
+
totalToolTimeMs: null,
|
|
168
|
+
toolCountsJson: null,
|
|
169
|
+
contextSummary: null,
|
|
170
|
+
lastCompactedAt: null,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
publish({
|
|
174
|
+
type: 'session.created',
|
|
175
|
+
sessionId: newSessionId,
|
|
176
|
+
payload: result,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
session: result,
|
|
181
|
+
parentSessionId,
|
|
182
|
+
branchPointMessageId: fromMessageId,
|
|
183
|
+
copiedMessages: messagesToCopy.length,
|
|
184
|
+
copiedParts,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export type ListBranchesResult = Array<{
|
|
189
|
+
session: SessionRow;
|
|
190
|
+
branchPointMessageId: string | null;
|
|
191
|
+
branchPointPreview: string | null;
|
|
192
|
+
createdAt: number;
|
|
193
|
+
}>;
|
|
194
|
+
|
|
195
|
+
export async function listBranches(
|
|
196
|
+
db: DB,
|
|
197
|
+
sessionId: string,
|
|
198
|
+
projectPath: string,
|
|
199
|
+
): Promise<ListBranchesResult> {
|
|
200
|
+
const branches = await db
|
|
201
|
+
.select()
|
|
202
|
+
.from(sessions)
|
|
203
|
+
.where(eq(sessions.parentSessionId, sessionId))
|
|
204
|
+
.orderBy(asc(sessions.createdAt));
|
|
205
|
+
|
|
206
|
+
const results: ListBranchesResult = [];
|
|
207
|
+
|
|
208
|
+
for (const branch of branches) {
|
|
209
|
+
if (branch.projectPath !== projectPath) continue;
|
|
210
|
+
|
|
211
|
+
let preview: string | null = null;
|
|
212
|
+
|
|
213
|
+
if (branch.branchPointMessageId) {
|
|
214
|
+
const msgRows = await db
|
|
215
|
+
.select()
|
|
216
|
+
.from(messages)
|
|
217
|
+
.where(eq(messages.id, branch.branchPointMessageId));
|
|
218
|
+
|
|
219
|
+
if (msgRows.length > 0) {
|
|
220
|
+
const parts = await db
|
|
221
|
+
.select()
|
|
222
|
+
.from(messageParts)
|
|
223
|
+
.where(eq(messageParts.messageId, branch.branchPointMessageId))
|
|
224
|
+
.orderBy(asc(messageParts.index));
|
|
225
|
+
|
|
226
|
+
for (const part of parts) {
|
|
227
|
+
if (part.type === 'text') {
|
|
228
|
+
try {
|
|
229
|
+
const content = JSON.parse(part.content || '{}');
|
|
230
|
+
if (content.text) {
|
|
231
|
+
preview = content.text.slice(0, 100);
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
} catch {}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
results.push({
|
|
241
|
+
session: branch,
|
|
242
|
+
branchPointMessageId: branch.branchPointMessageId,
|
|
243
|
+
branchPointPreview: preview,
|
|
244
|
+
createdAt: branch.createdAt,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return results;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function getParentSession(
|
|
252
|
+
db: DB,
|
|
253
|
+
sessionId: string,
|
|
254
|
+
projectPath: string,
|
|
255
|
+
): Promise<SessionRow | null> {
|
|
256
|
+
const sessionRows = await db
|
|
257
|
+
.select()
|
|
258
|
+
.from(sessions)
|
|
259
|
+
.where(eq(sessions.id, sessionId));
|
|
260
|
+
|
|
261
|
+
if (!sessionRows.length) return null;
|
|
262
|
+
|
|
263
|
+
const session = sessionRows[0];
|
|
264
|
+
if (!session.parentSessionId) return null;
|
|
265
|
+
|
|
266
|
+
const parentRows = await db
|
|
267
|
+
.select()
|
|
268
|
+
.from(sessions)
|
|
269
|
+
.where(eq(sessions.id, session.parentSessionId));
|
|
270
|
+
|
|
271
|
+
if (!parentRows.length) return null;
|
|
272
|
+
|
|
273
|
+
const parent = parentRows[0];
|
|
274
|
+
if (parent.projectPath !== projectPath) return null;
|
|
275
|
+
|
|
276
|
+
return parent;
|
|
277
|
+
}
|