@deepseek-ai/dsh-api-session-controller 0.1.3-alpha.2 → 0.1.5-alpha.1
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.i18n.yaml +2 -2
- package/README.md +12 -2
- package/README.zh.md +12 -2
- package/lib/client.js +246 -30
- package/lib/index.js +107 -29
- package/lib/typert.host.js +71 -55
- package/lib/typert.remote-client.js +34 -34
- package/lib/types/agent.js +6 -8
- package/lib/types/client/session-wire-event.d.ts +11 -0
- package/lib/types/client/session-wire-event.js +43 -0
- package/lib/types/client/transport.js +6 -0
- package/lib/types/commands.d.ts +1 -1
- package/lib/types/commands.js +14 -4
- package/lib/types/control.d.ts +0 -1
- package/lib/types/control.js +19 -22
- package/lib/types/index.js +2 -0
- package/lib/types/media-references.d.ts +16 -0
- package/lib/types/media-references.js +77 -0
- package/lib/types/types.d.ts +15 -7
- package/package.json +71 -64
package/lib/index.js
CHANGED
|
@@ -15,6 +15,9 @@ import { assertNever } from "@deepseek-ai/dsh-util-values";
|
|
|
15
15
|
import { Deque } from "@deepseek-ai/dsh-deque";
|
|
16
16
|
import { z as z$1 } from "zod";
|
|
17
17
|
import { isUserInvocable } from "@deepseek-ai/dsh-skill";
|
|
18
|
+
import { isAbsolute } from "node:path";
|
|
19
|
+
import { FsError } from "@deepseek-ai/dsh-fs";
|
|
20
|
+
import mime from "mime-types";
|
|
18
21
|
//#region lib/types/agent.js
|
|
19
22
|
/** Agent activation, composition, and model-selection policy owned by API Session. */
|
|
20
23
|
var __addDisposableResource$4 = function(env, value, async) {
|
|
@@ -349,14 +352,14 @@ var ApiSessionAgentController = class {
|
|
|
349
352
|
*/
|
|
350
353
|
async composeAgent(presetId) {
|
|
351
354
|
const presets = this.ctx.get("agentPresets");
|
|
352
|
-
if (presets === void 0) return { setup: (
|
|
353
|
-
this.installSelection(
|
|
355
|
+
if (presets === void 0) return { setup: (_agentCtx, agent) => {
|
|
356
|
+
this.installSelection(agent);
|
|
354
357
|
} };
|
|
355
358
|
const resolvedId = (await presets.resolve(presetId)).id;
|
|
356
359
|
return {
|
|
357
360
|
agentPreset: resolvedId,
|
|
358
|
-
setup: async (agentCtx) => {
|
|
359
|
-
this.installSelection(
|
|
361
|
+
setup: async (agentCtx, agent) => {
|
|
362
|
+
this.installSelection(agent);
|
|
360
363
|
await presets.mount(agentCtx, resolvedId);
|
|
361
364
|
}
|
|
362
365
|
};
|
|
@@ -456,9 +459,7 @@ var ApiSessionAgentController = class {
|
|
|
456
459
|
model
|
|
457
460
|
};
|
|
458
461
|
}
|
|
459
|
-
installSelection(
|
|
460
|
-
const agent = agentCtx.agent;
|
|
461
|
-
if (agent === void 0) throw new Error("api-session: Agent setup has no scoped Agent");
|
|
462
|
+
installSelection(agent) {
|
|
462
463
|
this.selectionFor(agent);
|
|
463
464
|
}
|
|
464
465
|
/**
|
|
@@ -543,6 +544,9 @@ var __disposeResources$3 = (function(SuppressedError) {
|
|
|
543
544
|
var e = new Error(message);
|
|
544
545
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
545
546
|
});
|
|
547
|
+
function hasPromptContent(content) {
|
|
548
|
+
return content.some((part) => part.type !== "text" || part.text.trim().length > 0);
|
|
549
|
+
}
|
|
546
550
|
/** Implements Session business commands delegated by the Session Controller Remote service. */
|
|
547
551
|
var SessionCommandController = class {
|
|
548
552
|
ctx;
|
|
@@ -724,11 +728,12 @@ var SessionCommandController = class {
|
|
|
724
728
|
}
|
|
725
729
|
}
|
|
726
730
|
/**
|
|
727
|
-
*
|
|
731
|
+
* Reject empty content, then admit one prompt after Agent and attachment validation.
|
|
728
732
|
* @param request - Session identity, prompt content, source metadata, and delivery mode.
|
|
729
733
|
* @returns acknowledgement that the Agent accepted the prompt.
|
|
730
734
|
*/
|
|
731
735
|
async prompt(request) {
|
|
736
|
+
if (!hasPromptContent(request.content)) throw new RemoteError("gateway/bad-request", "prompt content must include non-whitespace text or an attachment", {});
|
|
732
737
|
const clientTimeZone = request.clientTimeZone === void 0 ? void 0 : canonicalClientTimeZone(request.clientTimeZone);
|
|
733
738
|
if (request.clientTimeZone !== void 0 && clientTimeZone === void 0) throw new RemoteError("session/invalid-time-zone", "clientTimeZone must be UTC or a valid IANA Area/Location name", { value: request.clientTimeZone });
|
|
734
739
|
const agent = await this.resolveAgent(request.sessionId);
|
|
@@ -814,7 +819,10 @@ var SessionCommandController = class {
|
|
|
814
819
|
* @returns acknowledgement that the queue mutation was applied.
|
|
815
820
|
*/
|
|
816
821
|
updateQueue(request) {
|
|
817
|
-
if (request.action.kind === "edit"
|
|
822
|
+
if (request.action.kind === "edit") {
|
|
823
|
+
if (request.action.content.some((block) => block.type !== "text")) throw new RemoteError("session/attachment-invalid", "queue edits accept text content only", { reason: "QUEUE_EDIT_NON_TEXT" });
|
|
824
|
+
if (!hasPromptContent(request.action.content)) throw new RemoteError("gateway/bad-request", "queue edit content must include non-whitespace text", {});
|
|
825
|
+
}
|
|
818
826
|
const agent = this.ctx.agents.get(request.sessionId);
|
|
819
827
|
if (agent === void 0) throw new RemoteError("session/queue-item-not-found", "queued item is no longer pending", { itemId: request.itemId });
|
|
820
828
|
if (hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) {
|
|
@@ -989,9 +997,6 @@ var SessionControlController = class {
|
|
|
989
997
|
/** @param ctx - Host context carrying live Agent, projection, and jobs services. */
|
|
990
998
|
constructor(ctx) {
|
|
991
999
|
this.ctx = ctx;
|
|
992
|
-
ctx.on("session/event", (session, event) => {
|
|
993
|
-
this.onSessionEvent(session, event);
|
|
994
|
-
});
|
|
995
1000
|
ctx.sessionProjections.onChanged((session, key, value, seq) => {
|
|
996
1001
|
this.broadcast({
|
|
997
1002
|
type: "projection",
|
|
@@ -1000,6 +1005,13 @@ var SessionControlController = class {
|
|
|
1000
1005
|
value,
|
|
1001
1006
|
seq
|
|
1002
1007
|
});
|
|
1008
|
+
if (key !== "inbox") return;
|
|
1009
|
+
if (this.ctx.agents.get(session.id)?.session !== session) return;
|
|
1010
|
+
this.broadcast({
|
|
1011
|
+
type: "queue",
|
|
1012
|
+
sessionId: session.id,
|
|
1013
|
+
items: queueItemsFromInbox(value)
|
|
1014
|
+
});
|
|
1003
1015
|
});
|
|
1004
1016
|
ctx.inject(["jobs"], (jobsCtx) => {
|
|
1005
1017
|
jobsCtx.jobs.onJobsChanged((owner) => {
|
|
@@ -1065,16 +1077,6 @@ var SessionControlController = class {
|
|
|
1065
1077
|
}
|
|
1066
1078
|
return blocks;
|
|
1067
1079
|
}
|
|
1068
|
-
onSessionEvent(session, event) {
|
|
1069
|
-
if (event.type !== "agent/inbox/spliced") return;
|
|
1070
|
-
const agent = this.ctx.agents.get(session.id);
|
|
1071
|
-
if (agent?.session !== session) return;
|
|
1072
|
-
this.broadcast({
|
|
1073
|
-
type: "queue",
|
|
1074
|
-
sessionId: session.id,
|
|
1075
|
-
items: queueItems(agent, event.data)
|
|
1076
|
-
});
|
|
1077
|
-
}
|
|
1078
1080
|
onJobsChanged(owner) {
|
|
1079
1081
|
if (owner !== void 0) {
|
|
1080
1082
|
this.broadcast({
|
|
@@ -1139,12 +1141,14 @@ var ControlQueue = class {
|
|
|
1139
1141
|
}
|
|
1140
1142
|
}
|
|
1141
1143
|
};
|
|
1142
|
-
function queueItems(agent
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
};
|
|
1147
|
-
|
|
1144
|
+
function queueItems(agent) {
|
|
1145
|
+
return queueItemsFromInbox({
|
|
1146
|
+
"next-turn": agent.inbox.nextTurn,
|
|
1147
|
+
"next-step": agent.inbox.nextStep
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
function queueItemsFromInbox(inbox) {
|
|
1151
|
+
return [...inbox["next-turn"].map((message) => ({
|
|
1148
1152
|
id: message.id,
|
|
1149
1153
|
placement: "queued",
|
|
1150
1154
|
...promptRpcId(message),
|
|
@@ -1152,7 +1156,7 @@ function queueItems(agent, splice) {
|
|
|
1152
1156
|
id: message.id,
|
|
1153
1157
|
content: message.content
|
|
1154
1158
|
}
|
|
1155
|
-
})), ...
|
|
1159
|
+
})), ...inbox["next-step"].map((message) => ({
|
|
1156
1160
|
id: message.id,
|
|
1157
1161
|
placement: message.source.kind === "user" ? "steering" : "context",
|
|
1158
1162
|
...promptRpcId(message),
|
|
@@ -2297,6 +2301,79 @@ let SessionSkillCatalog = (() => {
|
|
|
2297
2301
|
};
|
|
2298
2302
|
})();
|
|
2299
2303
|
//#endregion
|
|
2304
|
+
//#region lib/types/media-references.js
|
|
2305
|
+
/**
|
|
2306
|
+
* Authenticated GET/HEAD /api/file reads bounded file responses through
|
|
2307
|
+
* the composed filesystem provider. Paths and MIME types do not restrict access;
|
|
2308
|
+
* the connection service authenticates requests before this handler.
|
|
2309
|
+
* @module @deepseek-ai/dsh-api-session-controller/media-references
|
|
2310
|
+
*/
|
|
2311
|
+
const BASE_HEADERS = {
|
|
2312
|
+
"Cache-Control": "private, no-store",
|
|
2313
|
+
"X-Content-Type-Options": "nosniff",
|
|
2314
|
+
"Content-Security-Policy": "sandbox; default-src 'none'"
|
|
2315
|
+
};
|
|
2316
|
+
async function serveFile(request, fs, maxBytes) {
|
|
2317
|
+
const fail = (status, text) => new Response(request.method === "HEAD" ? null : text, {
|
|
2318
|
+
status,
|
|
2319
|
+
headers: BASE_HEADERS
|
|
2320
|
+
});
|
|
2321
|
+
const path = new URL(request.url).searchParams.get("path");
|
|
2322
|
+
if (path === null || path.length === 0) return fail(400, "missing path");
|
|
2323
|
+
if (path.includes("\0") || !isAbsolute(path)) return fail(400, "absolute path required");
|
|
2324
|
+
try {
|
|
2325
|
+
const target = await fs.resolve(path, { signal: request.signal });
|
|
2326
|
+
const mediaType = mime.lookup(target.displayPath) || "application/octet-stream";
|
|
2327
|
+
const headers = {
|
|
2328
|
+
...BASE_HEADERS,
|
|
2329
|
+
"Content-Type": mediaType
|
|
2330
|
+
};
|
|
2331
|
+
if (request.method === "HEAD") {
|
|
2332
|
+
const info = await fs.stat(target, request.signal);
|
|
2333
|
+
if (info === void 0) return fail(404, "not found");
|
|
2334
|
+
if (info.type !== "file") return fail(403, "not a regular file");
|
|
2335
|
+
if (info.size !== void 0) {
|
|
2336
|
+
if (info.size > maxBytes) return fail(413, "file exceeds byte limit");
|
|
2337
|
+
headers["Content-Length"] = String(info.size);
|
|
2338
|
+
}
|
|
2339
|
+
return new Response(null, { headers });
|
|
2340
|
+
}
|
|
2341
|
+
const bytes = await fs.readBytes(target, request.signal, maxBytes);
|
|
2342
|
+
headers["Content-Length"] = String(bytes.byteLength);
|
|
2343
|
+
return new Response(bytes.slice(), { headers });
|
|
2344
|
+
} catch (error) {
|
|
2345
|
+
if (!(error instanceof FsError)) throw error;
|
|
2346
|
+
return fail({
|
|
2347
|
+
FS_NOT_FOUND: 404,
|
|
2348
|
+
FS_NOT_REGULAR_FILE: 403,
|
|
2349
|
+
FS_PERMISSION_DENIED: 403,
|
|
2350
|
+
FS_SANDBOX_DENIED: 403,
|
|
2351
|
+
FS_TOO_LARGE: 413,
|
|
2352
|
+
FS_ABORTED: 499
|
|
2353
|
+
}[error.code] ?? 500, error.code);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
/**
|
|
2357
|
+
* File-display contribution. The connection service supplies authentication;
|
|
2358
|
+
* `ctx.fs` supplies the execution world's paths, reads, and access policy.
|
|
2359
|
+
*/
|
|
2360
|
+
const SessionMediaReferences = {
|
|
2361
|
+
inject: [
|
|
2362
|
+
"connection",
|
|
2363
|
+
"fs",
|
|
2364
|
+
"attachments"
|
|
2365
|
+
],
|
|
2366
|
+
apply(ctx) {
|
|
2367
|
+
const maxBytes = ctx.attachments.imageLimits.maxImageBytes;
|
|
2368
|
+
ctx.effect(() => ctx.connection.fetch.register({
|
|
2369
|
+
path: "/api/file",
|
|
2370
|
+
methods: ["GET", "HEAD"],
|
|
2371
|
+
requestBody: "buffered",
|
|
2372
|
+
fetch: (request) => serveFile(request, ctx.fs, maxBytes)
|
|
2373
|
+
}), "session-controller: /api/file");
|
|
2374
|
+
}
|
|
2375
|
+
};
|
|
2376
|
+
//#endregion
|
|
2300
2377
|
//#region lib/types/index.js
|
|
2301
2378
|
/** Session Remote owner: cold reads, explicit Agent commands, and live control state. */
|
|
2302
2379
|
var __runInitializers = function(thisArg, initializers, value) {
|
|
@@ -2664,6 +2741,7 @@ let SessionController = (() => {
|
|
|
2664
2741
|
this.openPath = internals.openPath ?? openNativePath;
|
|
2665
2742
|
this.canOpenPath = internals.canOpenPath ?? (() => config.nativeOpen ?? (internals.openPath !== void 0 || canOpenNativePath()));
|
|
2666
2743
|
ctx.plugin(SessionFileReferences);
|
|
2744
|
+
ctx.plugin(SessionMediaReferences);
|
|
2667
2745
|
ctx.plugin(SessionSkillCatalog);
|
|
2668
2746
|
ctx.on("session/created", (session) => {
|
|
2669
2747
|
ctx.emit("api-session/added", this.listState.summaryFor(session));
|