@sleep2agi/dsh-anet 0.0.0-stage → 0.1.0-preview.26
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 +60 -2
- package/README.zh.md +59 -0
- package/cordis.patch.yml +8 -0
- package/package.json +68 -4
- package/scripts/connect.mjs +84 -0
- package/src/client.js +433 -0
- package/src/hub-client.mjs +141 -0
- package/src/ledger.mjs +31 -0
- package/src/node-loop.mjs +185 -0
- package/src/nodes.mjs +107 -0
- package/src/plugin.mjs +324 -0
- package/src/tools.mjs +52 -0
- package/src/turn.mjs +40 -0
- package/src/ui-server.mjs +86 -0
package/src/turn.mjs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Turn outcome projection, independent of DSH so it can be unit-tested.
|
|
2
|
+
// Must stay in sync with the session-event shapes of the DSH build the plugin
|
|
3
|
+
// is installed into (0.1.0-rc.6: `session.events`, events carry numeric `seq`).
|
|
4
|
+
|
|
5
|
+
export const describe = (v) => {
|
|
6
|
+
if (v == null) return "";
|
|
7
|
+
if (typeof v === "string") return v;
|
|
8
|
+
if (v instanceof Error) return v.message;
|
|
9
|
+
try { return JSON.stringify(v).slice(0, 400); } catch { return String(v); }
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Aggregate the last assistant text, the turn outcome, and any error events
|
|
14
|
+
* produced after `firstSeq`. Mirrors dsh-headless's summarizer.
|
|
15
|
+
* Supports both the rc.6 `session.events` log and the older
|
|
16
|
+
* `session.eventAt(SessionSeq(n))` accessor.
|
|
17
|
+
*/
|
|
18
|
+
export function summarizeTurn(session, firstSeq) {
|
|
19
|
+
let started = false; let text = ""; let reason; const errors = [];
|
|
20
|
+
const events = Array.isArray(session.events)
|
|
21
|
+
? session.events.filter((event) => event.seq >= firstSeq)
|
|
22
|
+
: Array.from({ length: Math.max(0, session.seq - firstSeq) }, (_, i) => session.eventAt?.(firstSeq + i)).filter(Boolean);
|
|
23
|
+
for (const event of events) {
|
|
24
|
+
if (event.type === "turn/start") { started = true; continue; }
|
|
25
|
+
if (!started) continue;
|
|
26
|
+
if (event.type === "assistant/message") {
|
|
27
|
+
const joined = event.data.message.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
28
|
+
if (joined !== "") text = joined;
|
|
29
|
+
}
|
|
30
|
+
if (event.type === "turn/end") reason = event.data.reason;
|
|
31
|
+
if (/error/i.test(event.type)) errors.push(describe(event.data));
|
|
32
|
+
}
|
|
33
|
+
return { text, reason, errors };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The human-readable failure line for a turn that produced no text. */
|
|
37
|
+
export function turnFailure(reason, errors) {
|
|
38
|
+
const detail = [describe(reason), ...errors].filter(Boolean).join("; ").slice(0, 600);
|
|
39
|
+
return `turn ended: ${reason.kind}${detail ? ` — ${detail}` : ""}`;
|
|
40
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Loopback API for the Settings → Plugins → ANet card. The official client only
|
|
2
|
+
// exposes settings for a fixed namespace allowlist, so the card manages the
|
|
3
|
+
// node registry through this server instead of requiring a patched app.
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
|
|
6
|
+
const LOOPBACK_ORIGIN = /^http:\/\/(?:127\.0\.0\.1|localhost):\d+$/;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {{port?: number, listNodes: () => object, createNode: (draft: object) => Promise<object>,
|
|
10
|
+
* updateNode: (id: string, draft: object) => Promise<object>, deleteNode: (id: string) => Promise<object>,
|
|
11
|
+
* login: (draft: object) => Promise<object>, logout: () => object, account: () => object, projects: () => Promise<object>, sessions: () => Promise<object>, defaults: () => object, log?: (m: string) => void}} opts
|
|
12
|
+
*/
|
|
13
|
+
export function startConfigServer(opts) {
|
|
14
|
+
const { port = 9460, listNodes, createNode, updateNode, deleteNode, login, logout, account, projects, sessions, defaults, log = () => {} } = opts;
|
|
15
|
+
const server = createServer(async (req, res) => {
|
|
16
|
+
const origin = req.headers.origin ?? "";
|
|
17
|
+
const allowed = origin === "" || LOOPBACK_ORIGIN.test(origin);
|
|
18
|
+
if (origin !== "" && LOOPBACK_ORIGIN.test(origin)) {
|
|
19
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
20
|
+
res.setHeader("Vary", "Origin");
|
|
21
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS");
|
|
22
|
+
res.setHeader("Access-Control-Allow-Headers", "content-type");
|
|
23
|
+
}
|
|
24
|
+
if (req.method === "OPTIONS") { res.writeHead(204).end(); return; }
|
|
25
|
+
if (!allowed) { json(res, 403, { ok: false, error: "origin not allowed" }); return; }
|
|
26
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
27
|
+
try {
|
|
28
|
+
if (req.method === "GET" && url.pathname === "/nodes") {
|
|
29
|
+
json(res, 200, { ok: true, ...listNodes(), defaults: defaults() });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (req.method === "GET" && url.pathname === "/account") {
|
|
33
|
+
json(res, 200, { ok: true, ...account() });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (req.method === "POST" && url.pathname === "/logout") {
|
|
37
|
+
json(res, 200, { ok: true, ...logout() });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (req.method === "GET" && url.pathname === "/sessions") {
|
|
41
|
+
json(res, 200, { ok: true, ...(await sessions()) });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (req.method === "GET" && url.pathname === "/projects") {
|
|
45
|
+
json(res, 200, { ok: true, ...(await projects()) });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (req.method === "POST" && url.pathname === "/login") {
|
|
49
|
+
json(res, 200, { ok: true, ...(await login(await body(req))) });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (req.method === "POST" && url.pathname === "/nodes") {
|
|
53
|
+
const draft = await body(req);
|
|
54
|
+
json(res, 200, { ok: true, node: await createNode(draft) });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (req.method === "PATCH" && url.pathname.startsWith("/nodes/")) {
|
|
58
|
+
const id = decodeURIComponent(url.pathname.slice("/nodes/".length));
|
|
59
|
+
json(res, 200, { ok: true, node: await updateNode(id, await body(req)) });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (req.method === "DELETE" && url.pathname.startsWith("/nodes/")) {
|
|
63
|
+
const id = decodeURIComponent(url.pathname.slice("/nodes/".length));
|
|
64
|
+
json(res, 200, { ok: true, ...(await deleteNode(id)) });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
json(res, 404, { ok: false, error: "not found" });
|
|
68
|
+
} catch (error) {
|
|
69
|
+
json(res, 400, { ok: false, error: error?.message ?? String(error) });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
server.on("error", (error) => log(`config UI server failed on 127.0.0.1:${port}: ${error.message}`));
|
|
73
|
+
server.listen(port, "127.0.0.1", () => log(`config UI on http://127.0.0.1:${port}`));
|
|
74
|
+
return { port, close: () => new Promise((resolve) => server.close(() => resolve())) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function json(res, status, payload) {
|
|
78
|
+
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
79
|
+
res.end(JSON.stringify(payload));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function body(req) {
|
|
83
|
+
let raw = "";
|
|
84
|
+
for await (const chunk of req) raw += chunk;
|
|
85
|
+
return JSON.parse(raw || "{}");
|
|
86
|
+
}
|