@deepseek-ai/dsh-api-session-controller 0.1.3-alpha.2 → 0.1.5-alpha.2
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 +248 -30
- package/lib/index.js +125 -31
- package/lib/typert.host.js +149 -58
- package/lib/typert.remote-client.js +74 -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.d.ts +12 -0
- package/lib/types/index.js +18 -2
- package/lib/types/media-references.d.ts +16 -0
- package/lib/types/media-references.js +77 -0
- package/lib/types/types.d.ts +17 -7
- package/package.json +71 -64
package/lib/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { hostname } from "node:os";
|
|
1
2
|
import z from "@deepseek-ai/schemastery";
|
|
2
3
|
import { AssistantStreamAccumulator, ReasoningEffortId, assistantStreamChunks, createUserMessage, errorChain, freezeMessage } from "@deepseek-ai/dsh-llm";
|
|
3
|
-
import { canOpenNativePath, openNativePath } from "@deepseek-ai/dsh-native-command";
|
|
4
|
+
import { canOpenNativePath, nativeFileManager, openNativePath, revealNativePath } from "@deepseek-ai/dsh-native-command";
|
|
4
5
|
import { Remote, RemoteError, TypertRemoteService, remoteErrorOf } from "@deepseek-ai/dsh-typert-protocol";
|
|
5
6
|
import { mkdir } from "node:fs/promises";
|
|
6
7
|
import { installModelSelection } from "@deepseek-ai/dsh-agent";
|
|
@@ -15,6 +16,9 @@ import { assertNever } from "@deepseek-ai/dsh-util-values";
|
|
|
15
16
|
import { Deque } from "@deepseek-ai/dsh-deque";
|
|
16
17
|
import { z as z$1 } from "zod";
|
|
17
18
|
import { isUserInvocable } from "@deepseek-ai/dsh-skill";
|
|
19
|
+
import { isAbsolute } from "node:path";
|
|
20
|
+
import { FsError } from "@deepseek-ai/dsh-fs";
|
|
21
|
+
import mime from "mime-types";
|
|
18
22
|
//#region lib/types/agent.js
|
|
19
23
|
/** Agent activation, composition, and model-selection policy owned by API Session. */
|
|
20
24
|
var __addDisposableResource$4 = function(env, value, async) {
|
|
@@ -349,14 +353,14 @@ var ApiSessionAgentController = class {
|
|
|
349
353
|
*/
|
|
350
354
|
async composeAgent(presetId) {
|
|
351
355
|
const presets = this.ctx.get("agentPresets");
|
|
352
|
-
if (presets === void 0) return { setup: (
|
|
353
|
-
this.installSelection(
|
|
356
|
+
if (presets === void 0) return { setup: (_agentCtx, agent) => {
|
|
357
|
+
this.installSelection(agent);
|
|
354
358
|
} };
|
|
355
359
|
const resolvedId = (await presets.resolve(presetId)).id;
|
|
356
360
|
return {
|
|
357
361
|
agentPreset: resolvedId,
|
|
358
|
-
setup: async (agentCtx) => {
|
|
359
|
-
this.installSelection(
|
|
362
|
+
setup: async (agentCtx, agent) => {
|
|
363
|
+
this.installSelection(agent);
|
|
360
364
|
await presets.mount(agentCtx, resolvedId);
|
|
361
365
|
}
|
|
362
366
|
};
|
|
@@ -456,9 +460,7 @@ var ApiSessionAgentController = class {
|
|
|
456
460
|
model
|
|
457
461
|
};
|
|
458
462
|
}
|
|
459
|
-
installSelection(
|
|
460
|
-
const agent = agentCtx.agent;
|
|
461
|
-
if (agent === void 0) throw new Error("api-session: Agent setup has no scoped Agent");
|
|
463
|
+
installSelection(agent) {
|
|
462
464
|
this.selectionFor(agent);
|
|
463
465
|
}
|
|
464
466
|
/**
|
|
@@ -543,6 +545,9 @@ var __disposeResources$3 = (function(SuppressedError) {
|
|
|
543
545
|
var e = new Error(message);
|
|
544
546
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
545
547
|
});
|
|
548
|
+
function hasPromptContent(content) {
|
|
549
|
+
return content.some((part) => part.type !== "text" || part.text.trim().length > 0);
|
|
550
|
+
}
|
|
546
551
|
/** Implements Session business commands delegated by the Session Controller Remote service. */
|
|
547
552
|
var SessionCommandController = class {
|
|
548
553
|
ctx;
|
|
@@ -724,11 +729,12 @@ var SessionCommandController = class {
|
|
|
724
729
|
}
|
|
725
730
|
}
|
|
726
731
|
/**
|
|
727
|
-
*
|
|
732
|
+
* Reject empty content, then admit one prompt after Agent and attachment validation.
|
|
728
733
|
* @param request - Session identity, prompt content, source metadata, and delivery mode.
|
|
729
734
|
* @returns acknowledgement that the Agent accepted the prompt.
|
|
730
735
|
*/
|
|
731
736
|
async prompt(request) {
|
|
737
|
+
if (!hasPromptContent(request.content)) throw new RemoteError("gateway/bad-request", "prompt content must include non-whitespace text or an attachment", {});
|
|
732
738
|
const clientTimeZone = request.clientTimeZone === void 0 ? void 0 : canonicalClientTimeZone(request.clientTimeZone);
|
|
733
739
|
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
740
|
const agent = await this.resolveAgent(request.sessionId);
|
|
@@ -814,7 +820,10 @@ var SessionCommandController = class {
|
|
|
814
820
|
* @returns acknowledgement that the queue mutation was applied.
|
|
815
821
|
*/
|
|
816
822
|
updateQueue(request) {
|
|
817
|
-
if (request.action.kind === "edit"
|
|
823
|
+
if (request.action.kind === "edit") {
|
|
824
|
+
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" });
|
|
825
|
+
if (!hasPromptContent(request.action.content)) throw new RemoteError("gateway/bad-request", "queue edit content must include non-whitespace text", {});
|
|
826
|
+
}
|
|
818
827
|
const agent = this.ctx.agents.get(request.sessionId);
|
|
819
828
|
if (agent === void 0) throw new RemoteError("session/queue-item-not-found", "queued item is no longer pending", { itemId: request.itemId });
|
|
820
829
|
if (hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) {
|
|
@@ -989,9 +998,6 @@ var SessionControlController = class {
|
|
|
989
998
|
/** @param ctx - Host context carrying live Agent, projection, and jobs services. */
|
|
990
999
|
constructor(ctx) {
|
|
991
1000
|
this.ctx = ctx;
|
|
992
|
-
ctx.on("session/event", (session, event) => {
|
|
993
|
-
this.onSessionEvent(session, event);
|
|
994
|
-
});
|
|
995
1001
|
ctx.sessionProjections.onChanged((session, key, value, seq) => {
|
|
996
1002
|
this.broadcast({
|
|
997
1003
|
type: "projection",
|
|
@@ -1000,6 +1006,13 @@ var SessionControlController = class {
|
|
|
1000
1006
|
value,
|
|
1001
1007
|
seq
|
|
1002
1008
|
});
|
|
1009
|
+
if (key !== "inbox") return;
|
|
1010
|
+
if (this.ctx.agents.get(session.id)?.session !== session) return;
|
|
1011
|
+
this.broadcast({
|
|
1012
|
+
type: "queue",
|
|
1013
|
+
sessionId: session.id,
|
|
1014
|
+
items: queueItemsFromInbox(value)
|
|
1015
|
+
});
|
|
1003
1016
|
});
|
|
1004
1017
|
ctx.inject(["jobs"], (jobsCtx) => {
|
|
1005
1018
|
jobsCtx.jobs.onJobsChanged((owner) => {
|
|
@@ -1065,16 +1078,6 @@ var SessionControlController = class {
|
|
|
1065
1078
|
}
|
|
1066
1079
|
return blocks;
|
|
1067
1080
|
}
|
|
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
1081
|
onJobsChanged(owner) {
|
|
1079
1082
|
if (owner !== void 0) {
|
|
1080
1083
|
this.broadcast({
|
|
@@ -1139,12 +1142,14 @@ var ControlQueue = class {
|
|
|
1139
1142
|
}
|
|
1140
1143
|
}
|
|
1141
1144
|
};
|
|
1142
|
-
function queueItems(agent
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
};
|
|
1147
|
-
|
|
1145
|
+
function queueItems(agent) {
|
|
1146
|
+
return queueItemsFromInbox({
|
|
1147
|
+
"next-turn": agent.inbox.nextTurn,
|
|
1148
|
+
"next-step": agent.inbox.nextStep
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
function queueItemsFromInbox(inbox) {
|
|
1152
|
+
return [...inbox["next-turn"].map((message) => ({
|
|
1148
1153
|
id: message.id,
|
|
1149
1154
|
placement: "queued",
|
|
1150
1155
|
...promptRpcId(message),
|
|
@@ -1152,7 +1157,7 @@ function queueItems(agent, splice) {
|
|
|
1152
1157
|
id: message.id,
|
|
1153
1158
|
content: message.content
|
|
1154
1159
|
}
|
|
1155
|
-
})), ...
|
|
1160
|
+
})), ...inbox["next-step"].map((message) => ({
|
|
1156
1161
|
id: message.id,
|
|
1157
1162
|
placement: message.source.kind === "user" ? "steering" : "context",
|
|
1158
1163
|
...promptRpcId(message),
|
|
@@ -2297,6 +2302,79 @@ let SessionSkillCatalog = (() => {
|
|
|
2297
2302
|
};
|
|
2298
2303
|
})();
|
|
2299
2304
|
//#endregion
|
|
2305
|
+
//#region lib/types/media-references.js
|
|
2306
|
+
/**
|
|
2307
|
+
* Authenticated GET/HEAD /api/file reads bounded file responses through
|
|
2308
|
+
* the composed filesystem provider. Paths and MIME types do not restrict access;
|
|
2309
|
+
* the connection service authenticates requests before this handler.
|
|
2310
|
+
* @module @deepseek-ai/dsh-api-session-controller/media-references
|
|
2311
|
+
*/
|
|
2312
|
+
const BASE_HEADERS = {
|
|
2313
|
+
"Cache-Control": "private, no-store",
|
|
2314
|
+
"X-Content-Type-Options": "nosniff",
|
|
2315
|
+
"Content-Security-Policy": "sandbox; default-src 'none'"
|
|
2316
|
+
};
|
|
2317
|
+
async function serveFile(request, fs, maxBytes) {
|
|
2318
|
+
const fail = (status, text) => new Response(request.method === "HEAD" ? null : text, {
|
|
2319
|
+
status,
|
|
2320
|
+
headers: BASE_HEADERS
|
|
2321
|
+
});
|
|
2322
|
+
const path = new URL(request.url).searchParams.get("path");
|
|
2323
|
+
if (path === null || path.length === 0) return fail(400, "missing path");
|
|
2324
|
+
if (path.includes("\0") || !isAbsolute(path)) return fail(400, "absolute path required");
|
|
2325
|
+
try {
|
|
2326
|
+
const target = await fs.resolve(path, { signal: request.signal });
|
|
2327
|
+
const mediaType = mime.lookup(target.displayPath) || "application/octet-stream";
|
|
2328
|
+
const headers = {
|
|
2329
|
+
...BASE_HEADERS,
|
|
2330
|
+
"Content-Type": mediaType
|
|
2331
|
+
};
|
|
2332
|
+
if (request.method === "HEAD") {
|
|
2333
|
+
const info = await fs.stat(target, request.signal);
|
|
2334
|
+
if (info === void 0) return fail(404, "not found");
|
|
2335
|
+
if (info.type !== "file") return fail(403, "not a regular file");
|
|
2336
|
+
if (info.size !== void 0) {
|
|
2337
|
+
if (info.size > maxBytes) return fail(413, "file exceeds byte limit");
|
|
2338
|
+
headers["Content-Length"] = String(info.size);
|
|
2339
|
+
}
|
|
2340
|
+
return new Response(null, { headers });
|
|
2341
|
+
}
|
|
2342
|
+
const bytes = await fs.readBytes(target, request.signal, maxBytes);
|
|
2343
|
+
headers["Content-Length"] = String(bytes.byteLength);
|
|
2344
|
+
return new Response(bytes.slice(), { headers });
|
|
2345
|
+
} catch (error) {
|
|
2346
|
+
if (!(error instanceof FsError)) throw error;
|
|
2347
|
+
return fail({
|
|
2348
|
+
FS_NOT_FOUND: 404,
|
|
2349
|
+
FS_NOT_REGULAR_FILE: 403,
|
|
2350
|
+
FS_PERMISSION_DENIED: 403,
|
|
2351
|
+
FS_SANDBOX_DENIED: 403,
|
|
2352
|
+
FS_TOO_LARGE: 413,
|
|
2353
|
+
FS_ABORTED: 499
|
|
2354
|
+
}[error.code] ?? 500, error.code);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
/**
|
|
2358
|
+
* File-display contribution. The connection service supplies authentication;
|
|
2359
|
+
* `ctx.fs` supplies the execution world's paths, reads, and access policy.
|
|
2360
|
+
*/
|
|
2361
|
+
const SessionMediaReferences = {
|
|
2362
|
+
inject: [
|
|
2363
|
+
"connection",
|
|
2364
|
+
"fs",
|
|
2365
|
+
"attachments"
|
|
2366
|
+
],
|
|
2367
|
+
apply(ctx) {
|
|
2368
|
+
const maxBytes = ctx.attachments.imageLimits.maxImageBytes;
|
|
2369
|
+
ctx.effect(() => ctx.connection.fetch.register({
|
|
2370
|
+
path: "/api/file",
|
|
2371
|
+
methods: ["GET", "HEAD"],
|
|
2372
|
+
requestBody: "buffered",
|
|
2373
|
+
fetch: (request) => serveFile(request, ctx.fs, maxBytes)
|
|
2374
|
+
}), "session-controller: /api/file");
|
|
2375
|
+
}
|
|
2376
|
+
};
|
|
2377
|
+
//#endregion
|
|
2300
2378
|
//#region lib/types/index.js
|
|
2301
2379
|
/** Session Remote owner: cold reads, explicit Agent commands, and live control state. */
|
|
2302
2380
|
var __runInitializers = function(thisArg, initializers, value) {
|
|
@@ -2636,6 +2714,7 @@ let SessionController = (() => {
|
|
|
2636
2714
|
history;
|
|
2637
2715
|
listState;
|
|
2638
2716
|
openPath;
|
|
2717
|
+
revealPath;
|
|
2639
2718
|
canOpenPath;
|
|
2640
2719
|
promotions = /* @__PURE__ */ new Set();
|
|
2641
2720
|
/**
|
|
@@ -2662,8 +2741,10 @@ let SessionController = (() => {
|
|
|
2662
2741
|
});
|
|
2663
2742
|
this.listState = new ApiSessionList(ctx);
|
|
2664
2743
|
this.openPath = internals.openPath ?? openNativePath;
|
|
2744
|
+
this.revealPath = internals.revealPath ?? revealNativePath;
|
|
2665
2745
|
this.canOpenPath = internals.canOpenPath ?? (() => config.nativeOpen ?? (internals.openPath !== void 0 || canOpenNativePath()));
|
|
2666
2746
|
ctx.plugin(SessionFileReferences);
|
|
2747
|
+
ctx.plugin(SessionMediaReferences);
|
|
2667
2748
|
ctx.plugin(SessionSkillCatalog);
|
|
2668
2749
|
ctx.on("session/created", (session) => {
|
|
2669
2750
|
ctx.emit("api-session/added", this.listState.summaryFor(session));
|
|
@@ -2784,6 +2865,18 @@ let SessionController = (() => {
|
|
|
2784
2865
|
return this.canOpenPath();
|
|
2785
2866
|
}
|
|
2786
2867
|
/**
|
|
2868
|
+
* Describe the serving desktop for authenticated file-action routes.
|
|
2869
|
+
* @returns Host name, configured availability, and platform-specific file-manager behavior.
|
|
2870
|
+
*/
|
|
2871
|
+
workspaceDesktop() {
|
|
2872
|
+
const fileManager = nativeFileManager();
|
|
2873
|
+
return {
|
|
2874
|
+
name: hostname(),
|
|
2875
|
+
available: fileManager !== null && this.canOpenPath(),
|
|
2876
|
+
fileManager
|
|
2877
|
+
};
|
|
2878
|
+
}
|
|
2879
|
+
/**
|
|
2787
2880
|
* Open one path prepared by a Session-aware caller on the Host desktop.
|
|
2788
2881
|
* @param request - path after best-effort Session workspace resolution.
|
|
2789
2882
|
* @param signal - caller lifetime; abort terminates the native command.
|
|
@@ -2794,7 +2887,8 @@ let SessionController = (() => {
|
|
|
2794
2887
|
if (request.path.length === 0) throw new RemoteError("gateway/bad-request", "session.openWorkspacePath requires a non-empty path", {});
|
|
2795
2888
|
signal.throwIfAborted();
|
|
2796
2889
|
try {
|
|
2797
|
-
await this.
|
|
2890
|
+
if (request.action === "reveal") await this.revealPath(request.path, signal);
|
|
2891
|
+
else await this.openPath(request.path, signal);
|
|
2798
2892
|
return { opened: true };
|
|
2799
2893
|
} catch (error) {
|
|
2800
2894
|
if (signal.aborted) throw new RemoteError("gateway/cancelled", "path open was aborted", {});
|