@sna-sdk/core 0.3.0 → 0.5.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/README.md +6 -0
- package/dist/core/providers/cc-history-adapter.d.ts +37 -0
- package/dist/core/providers/cc-history-adapter.js +70 -0
- package/dist/core/providers/claude-code.js +50 -27
- package/dist/core/providers/types.d.ts +7 -1
- package/dist/db/schema.js +1 -1
- package/dist/scripts/sna.js +20 -2
- package/dist/scripts/tu-oneshot.d.ts +2 -0
- package/dist/scripts/tu-oneshot.js +66 -0
- package/dist/server/api-types.d.ts +13 -0
- package/dist/server/history-builder.d.ts +16 -0
- package/dist/server/history-builder.js +25 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +2 -0
- package/dist/server/routes/agent.js +105 -17
- package/dist/server/session-manager.d.ts +23 -3
- package/dist/server/session-manager.js +67 -13
- package/dist/server/standalone.js +442 -87
- package/dist/server/ws.js +107 -5
- package/dist/testing/mock-api.js +20 -0
- package/package.json +1 -1
package/dist/server/ws.js
CHANGED
|
@@ -4,6 +4,7 @@ import { getDb } from "../db/schema.js";
|
|
|
4
4
|
import { logger } from "../lib/logger.js";
|
|
5
5
|
import { runOnce } from "./routes/agent.js";
|
|
6
6
|
import { wsReply } from "./api-types.js";
|
|
7
|
+
import { buildHistoryFromDb } from "./history-builder.js";
|
|
7
8
|
import { saveImages } from "./image-store.js";
|
|
8
9
|
function send(ws, data) {
|
|
9
10
|
if (ws.readyState === ws.OPEN) {
|
|
@@ -30,13 +31,16 @@ function attachWebSocket(server, sessionManager) {
|
|
|
30
31
|
});
|
|
31
32
|
wss.on("connection", (ws) => {
|
|
32
33
|
logger.log("ws", "client connected");
|
|
33
|
-
const state = { agentUnsubs: /* @__PURE__ */ new Map(), skillEventUnsub: null, skillPollTimer: null, permissionUnsub: null, lifecycleUnsub: null, configChangedUnsub: null };
|
|
34
|
+
const state = { agentUnsubs: /* @__PURE__ */ new Map(), skillEventUnsub: null, skillPollTimer: null, permissionUnsub: null, lifecycleUnsub: null, configChangedUnsub: null, stateChangedUnsub: null };
|
|
34
35
|
state.lifecycleUnsub = sessionManager.onSessionLifecycle((event) => {
|
|
35
36
|
send(ws, { type: "session.lifecycle", ...event });
|
|
36
37
|
});
|
|
37
38
|
state.configChangedUnsub = sessionManager.onConfigChanged((event) => {
|
|
38
39
|
send(ws, { type: "session.config-changed", ...event });
|
|
39
40
|
});
|
|
41
|
+
state.stateChangedUnsub = sessionManager.onStateChanged((event) => {
|
|
42
|
+
send(ws, { type: "session.state-changed", ...event });
|
|
43
|
+
});
|
|
40
44
|
ws.on("message", (raw) => {
|
|
41
45
|
let msg;
|
|
42
46
|
try {
|
|
@@ -67,6 +71,8 @@ function attachWebSocket(server, sessionManager) {
|
|
|
67
71
|
state.lifecycleUnsub = null;
|
|
68
72
|
state.configChangedUnsub?.();
|
|
69
73
|
state.configChangedUnsub = null;
|
|
74
|
+
state.stateChangedUnsub?.();
|
|
75
|
+
state.stateChangedUnsub = null;
|
|
70
76
|
});
|
|
71
77
|
});
|
|
72
78
|
return wss;
|
|
@@ -85,6 +91,8 @@ function handleMessage(ws, msg, sm, state) {
|
|
|
85
91
|
return handleAgentStart(ws, msg, sm);
|
|
86
92
|
case "agent.send":
|
|
87
93
|
return handleAgentSend(ws, msg, sm);
|
|
94
|
+
case "agent.resume":
|
|
95
|
+
return handleAgentResume(ws, msg, sm);
|
|
88
96
|
case "agent.restart":
|
|
89
97
|
return handleAgentRestart(ws, msg, sm);
|
|
90
98
|
case "agent.interrupt":
|
|
@@ -226,7 +234,13 @@ function handleAgentSend(ws, msg, sm) {
|
|
|
226
234
|
db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, textContent, Object.keys(meta).length > 0 ? JSON.stringify(meta) : null);
|
|
227
235
|
} catch {
|
|
228
236
|
}
|
|
229
|
-
|
|
237
|
+
sm.pushEvent(sessionId, {
|
|
238
|
+
type: "user_message",
|
|
239
|
+
message: textContent,
|
|
240
|
+
data: Object.keys(meta).length > 0 ? meta : void 0,
|
|
241
|
+
timestamp: Date.now()
|
|
242
|
+
});
|
|
243
|
+
sm.updateSessionState(sessionId, "processing");
|
|
230
244
|
sm.touch(sessionId);
|
|
231
245
|
if (images?.length) {
|
|
232
246
|
const content = [
|
|
@@ -242,6 +256,43 @@ function handleAgentSend(ws, msg, sm) {
|
|
|
242
256
|
}
|
|
243
257
|
wsReply(ws, msg, { status: "sent" });
|
|
244
258
|
}
|
|
259
|
+
function handleAgentResume(ws, msg, sm) {
|
|
260
|
+
const sessionId = msg.session ?? "default";
|
|
261
|
+
const session = sm.getOrCreateSession(sessionId);
|
|
262
|
+
if (session.process?.alive) {
|
|
263
|
+
return replyError(ws, msg, "Session already running. Use agent.send instead.");
|
|
264
|
+
}
|
|
265
|
+
const history = buildHistoryFromDb(sessionId);
|
|
266
|
+
if (history.length === 0 && !msg.prompt) {
|
|
267
|
+
return replyError(ws, msg, "No history in DB \u2014 nothing to resume.");
|
|
268
|
+
}
|
|
269
|
+
const providerName = msg.provider ?? session.lastStartConfig?.provider ?? "claude-code";
|
|
270
|
+
const model = msg.model ?? session.lastStartConfig?.model ?? "claude-sonnet-4-6";
|
|
271
|
+
const permissionMode = msg.permissionMode ?? session.lastStartConfig?.permissionMode ?? "acceptEdits";
|
|
272
|
+
const extraArgs = msg.extraArgs ?? session.lastStartConfig?.extraArgs;
|
|
273
|
+
const provider = getProvider(providerName);
|
|
274
|
+
try {
|
|
275
|
+
const proc = provider.spawn({
|
|
276
|
+
cwd: session.cwd,
|
|
277
|
+
prompt: msg.prompt,
|
|
278
|
+
model,
|
|
279
|
+
permissionMode,
|
|
280
|
+
env: { SNA_SESSION_ID: sessionId },
|
|
281
|
+
history: history.length > 0 ? history : void 0,
|
|
282
|
+
extraArgs
|
|
283
|
+
});
|
|
284
|
+
sm.setProcess(sessionId, proc, "resumed");
|
|
285
|
+
sm.saveStartConfig(sessionId, { provider: providerName, model, permissionMode, extraArgs });
|
|
286
|
+
wsReply(ws, msg, {
|
|
287
|
+
status: "resumed",
|
|
288
|
+
provider: providerName,
|
|
289
|
+
sessionId: session.id,
|
|
290
|
+
historyCount: history.length
|
|
291
|
+
});
|
|
292
|
+
} catch (e) {
|
|
293
|
+
replyError(ws, msg, e.message);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
245
296
|
function handleAgentRestart(ws, msg, sm) {
|
|
246
297
|
const sessionId = msg.session ?? "default";
|
|
247
298
|
try {
|
|
@@ -298,11 +349,25 @@ function handleAgentKill(ws, msg, sm) {
|
|
|
298
349
|
function handleAgentStatus(ws, msg, sm) {
|
|
299
350
|
const sessionId = msg.session ?? "default";
|
|
300
351
|
const session = sm.getSession(sessionId);
|
|
352
|
+
const alive = session?.process?.alive ?? false;
|
|
353
|
+
let messageCount = 0;
|
|
354
|
+
let lastMessage = null;
|
|
355
|
+
try {
|
|
356
|
+
const db = getDb();
|
|
357
|
+
const count = db.prepare("SELECT COUNT(*) as c FROM chat_messages WHERE session_id = ?").get(sessionId);
|
|
358
|
+
messageCount = count?.c ?? 0;
|
|
359
|
+
const last = db.prepare("SELECT role, content, created_at FROM chat_messages WHERE session_id = ? ORDER BY id DESC LIMIT 1").get(sessionId);
|
|
360
|
+
if (last) lastMessage = { role: last.role, content: last.content, created_at: last.created_at };
|
|
361
|
+
} catch {
|
|
362
|
+
}
|
|
301
363
|
wsReply(ws, msg, {
|
|
302
|
-
alive
|
|
364
|
+
alive,
|
|
365
|
+
agentStatus: !alive ? "disconnected" : session?.state === "processing" ? "busy" : "idle",
|
|
303
366
|
sessionId: session?.process?.sessionId ?? null,
|
|
304
367
|
ccSessionId: session?.ccSessionId ?? null,
|
|
305
368
|
eventCount: session?.eventCounter ?? 0,
|
|
369
|
+
messageCount,
|
|
370
|
+
lastMessage,
|
|
306
371
|
config: session?.lastStartConfig ?? null
|
|
307
372
|
});
|
|
308
373
|
}
|
|
@@ -319,7 +384,38 @@ function handleAgentSubscribe(ws, msg, sm, state) {
|
|
|
319
384
|
const sessionId = msg.session ?? "default";
|
|
320
385
|
const session = sm.getOrCreateSession(sessionId);
|
|
321
386
|
state.agentUnsubs.get(sessionId)?.();
|
|
322
|
-
|
|
387
|
+
const includeHistory = msg.since === 0 || msg.includeHistory === true;
|
|
388
|
+
let cursor = 0;
|
|
389
|
+
if (includeHistory) {
|
|
390
|
+
try {
|
|
391
|
+
const db = getDb();
|
|
392
|
+
const rows = db.prepare(
|
|
393
|
+
`SELECT role, content, meta, created_at FROM chat_messages
|
|
394
|
+
WHERE session_id = ? ORDER BY id ASC`
|
|
395
|
+
).all(sessionId);
|
|
396
|
+
for (const row of rows) {
|
|
397
|
+
cursor++;
|
|
398
|
+
const eventType = row.role === "user" ? "user_message" : row.role === "assistant" ? "assistant" : row.role === "thinking" ? "thinking" : row.role === "tool" ? "tool_use" : row.role === "tool_result" ? "tool_result" : row.role === "error" ? "error" : null;
|
|
399
|
+
if (!eventType) continue;
|
|
400
|
+
const meta = row.meta ? JSON.parse(row.meta) : void 0;
|
|
401
|
+
send(ws, {
|
|
402
|
+
type: "agent.event",
|
|
403
|
+
session: sessionId,
|
|
404
|
+
cursor,
|
|
405
|
+
isHistory: true,
|
|
406
|
+
event: {
|
|
407
|
+
type: eventType,
|
|
408
|
+
message: row.content,
|
|
409
|
+
data: meta,
|
|
410
|
+
timestamp: new Date(row.created_at).getTime()
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
} catch {
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const bufferStart = typeof msg.since === "number" && msg.since > 0 ? msg.since : session.eventCounter;
|
|
418
|
+
if (!includeHistory) cursor = bufferStart;
|
|
323
419
|
if (cursor < session.eventCounter) {
|
|
324
420
|
const startIdx = Math.max(0, session.eventBuffer.length - (session.eventCounter - cursor));
|
|
325
421
|
const events = session.eventBuffer.slice(startIdx);
|
|
@@ -327,6 +423,8 @@ function handleAgentSubscribe(ws, msg, sm, state) {
|
|
|
327
423
|
cursor++;
|
|
328
424
|
send(ws, { type: "agent.event", session: sessionId, cursor, event });
|
|
329
425
|
}
|
|
426
|
+
} else {
|
|
427
|
+
cursor = session.eventCounter;
|
|
330
428
|
}
|
|
331
429
|
const unsub = sm.onSessionEvent(sessionId, (eventCursor, event) => {
|
|
332
430
|
send(ws, { type: "agent.event", session: sessionId, cursor: eventCursor, event });
|
|
@@ -439,10 +537,14 @@ function handlePermissionPending(ws, msg, sm) {
|
|
|
439
537
|
}
|
|
440
538
|
function handlePermissionSubscribe(ws, msg, sm, state) {
|
|
441
539
|
state.permissionUnsub?.();
|
|
540
|
+
const pending = sm.getAllPendingPermissions();
|
|
541
|
+
for (const p of pending) {
|
|
542
|
+
send(ws, { type: "permission.request", session: p.sessionId, request: p.request, createdAt: p.createdAt, isHistory: true });
|
|
543
|
+
}
|
|
442
544
|
state.permissionUnsub = sm.onPermissionRequest((sessionId, request, createdAt) => {
|
|
443
545
|
send(ws, { type: "permission.request", session: sessionId, request, createdAt });
|
|
444
546
|
});
|
|
445
|
-
reply(ws, msg, {});
|
|
547
|
+
reply(ws, msg, { pendingCount: pending.length });
|
|
446
548
|
}
|
|
447
549
|
function handlePermissionUnsubscribe(ws, msg, state) {
|
|
448
550
|
state.permissionUnsub?.();
|
package/dist/testing/mock-api.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import http from "http";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
2
4
|
function ts() {
|
|
3
5
|
return (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
4
6
|
}
|
|
@@ -35,6 +37,24 @@ async function startMockAnthropicServer() {
|
|
|
35
37
|
const realText = textBlocks.find((t) => !t.startsWith("<system-reminder>"));
|
|
36
38
|
userText = realText ?? textBlocks[textBlocks.length - 1] ?? "(no text)";
|
|
37
39
|
}
|
|
40
|
+
console.log(`[${ts()}] BODY KEYS: ${Object.keys(body).join(", ")}`);
|
|
41
|
+
try {
|
|
42
|
+
const dumpPath = path.join(process.cwd(), ".sna/mock-api-last-request.json");
|
|
43
|
+
fs.writeFileSync(dumpPath, JSON.stringify(body, null, 2));
|
|
44
|
+
console.log(`[${ts()}] FULL BODY dumped to .sna/mock-api-last-request.json`);
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
if (body.system) {
|
|
48
|
+
const sysText = typeof body.system === "string" ? body.system : JSON.stringify(body.system);
|
|
49
|
+
console.log(`[${ts()}] SYSTEM PROMPT (${sysText.length} chars): ${sysText.slice(0, 300)}...`);
|
|
50
|
+
if (sysText.includes("\uC720\uB2C8") || sysText.includes("\uCEE4\uD53C") || sysText.includes("\uAE30\uC5B5")) {
|
|
51
|
+
console.log(`[${ts()}] *** HISTORY FOUND IN SYSTEM PROMPT ***`);
|
|
52
|
+
for (const keyword of ["\uC720\uB2C8", "\uCEE4\uD53C", "\uAE30\uC5B5"]) {
|
|
53
|
+
const idx = sysText.indexOf(keyword);
|
|
54
|
+
if (idx >= 0) console.log(`[${ts()}] "${keyword}" at pos ${idx}: ...${sysText.slice(Math.max(0, idx - 50), idx + 80)}...`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
38
58
|
console.log(`[${ts()}] REQ model=${body.model} stream=${body.stream} messages=${body.messages?.length} user="${userText.slice(0, 120)}"`);
|
|
39
59
|
for (let mi = 0; mi < body.messages.length; mi++) {
|
|
40
60
|
const m = body.messages[mi];
|