dsh-bridge 0.1.0-rc.10

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 ADDED
@@ -0,0 +1,68 @@
1
+ # dsh-bridge
2
+
3
+ > Local, cross-session messaging for DeepSeek Harness.
4
+
5
+ **DSH Bridge** is the local messaging foundation of the DSH family. It lets live
6
+ sessions in one DSH host discover one another and exchange messages. It has no
7
+ Web UI and no network transport.
8
+
9
+ `dsh-weave` can extend Bridge across machines; `dsh-chat` can present its
10
+ messages as a human-facing group chat. Neither is required for local use.
11
+
12
+ The plugin exposes the `ctx.dshBridge` service and registers `session_list`,
13
+ `session_send`, and `session_messages` for agents. The old `ctx.sessionMessaging`
14
+ accessor remains as a temporary compatibility alias.
15
+
16
+ `ctx.dshBridge.deliverExternal()` is the controlled inbound seam for a trusted
17
+ transport such as Weave: it emits the same session follow-up and audit record
18
+ as local delivery, rather than letting a transport manipulate agents directly.
19
+ Delivery uses the public `ctx.agents` registry and `Agent.followup()`. An idle
20
+ target is woken, a running target receives ordinary queued work, and a persisted
21
+ offline target is resumed through DSH's configured Host agent resolver before
22
+ delivery. Concurrent messages to the same cold session share one resume operation.
23
+ The resolver reconstructs the recorded agent preset and model selection exactly
24
+ as the Web host does. A bounded
25
+ in-memory recent log (the latest 1,000 delivered messages) is kept only for
26
+ `session_messages` replay and diagnostics; it is not a second delivery queue.
27
+ Messages carry sender, target, UUID, and timestamp metadata.
28
+
29
+ `ctx.dshBridge.status(sessionId)` reports a session's presentation state:
30
+ `waking` while a cold resume is in flight, `archived` when the id is in the
31
+ workspace registry's archive set, `idle`/`running` for live agents, `offline`
32
+ for persisted sessions, and `missing` otherwise. Archive takes precedence over
33
+ live presence: archiving a session hides it from live surfaces (such as
34
+ dsh-chat room member indicators) without stopping its agent. Delivery to an
35
+ archived session is rejected: `session_send` and `deliverExternal` refuse to
36
+ wake or reach an archived target, so an archived agent stops receiving messages
37
+ entirely rather than merely dropping off live surfaces.
38
+
39
+ This package intentionally does not implement cross-host transport. `dsh-weave`
40
+ will provide the authenticated network backend while preserving the local
41
+ message semantics.
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ dsh plugin --profile web add dsh-bridge@next
47
+ ```
48
+
49
+ ## Known Limitations and Deferred Work
50
+
51
+ - **Process boundary** — sessions in another process or host are not visible;
52
+ add an authenticated relay/backend before advertising cross-host delivery.
53
+ - **In-memory retention** — messages are lost when the plugin process exits and
54
+ older than the latest 1,000 are evicted; durable inbox/outbox persistence is
55
+ still deferred until a cross-process relay needs it.
56
+ - **Delivery acknowledgement** — the current result means the target was live
57
+ (or successfully resumed) and accepted the follow-up call, not that the target
58
+ model processed it.
59
+
60
+ ## Model Experience
61
+
62
+ None, as `session_list`, `session_send`, and `session_messages` expose their
63
+ schemas directly through the tool registry.
64
+
65
+ ### KV Cache effect
66
+
67
+ Independent tool schemas; sending a message changes only the target session's
68
+ queued input and does not alter the sender's cached prompt prefix.
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: dsh-bridge
3
+ name: dsh-bridge
package/lib/index.js ADDED
@@ -0,0 +1,240 @@
1
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
2
+ import { SessionId } from "@deepseek-ai/dsh-session";
3
+ import { defineTool } from "@deepseek-ai/dsh-tools";
4
+ //#region lib/types/index.js
5
+ const name = "dsh-bridge";
6
+ const inject = ["agents", "tools", "sessionPersistence", "workspaceRegistry", "typert"];
7
+ const MAX_RECENT_MESSAGES = 1000;
8
+ class LocalSessionMessagingImpl {
9
+ ctx;
10
+ messages = [];
11
+ listeners = /* @__PURE__ */ new Set();
12
+ resuming = /* @__PURE__ */ new Map();
13
+ constructor(ctx) {
14
+ this.ctx = ctx;
15
+ }
16
+ list() {
17
+ return [...this.ctx.agents.list()].map((agent) => agent.session.id);
18
+ }
19
+ async status(sessionId) {
20
+ const id = String(sessionId);
21
+ if (this.resuming.has(id)) return { sessionId: id, state: "waking", live: false };
22
+ // Archive is a durable hide flag: it masks live presence so an archived
23
+ // session never advertises as running/idle, even while its agent keeps
24
+ // working in the background.
25
+ if (new Set(this.ctx.workspaceRegistry?.archivedSessionIds ?? []).has(id)) return { sessionId: id, state: "archived", live: false };
26
+ const live = this.ctx.agents.get(sessionId);
27
+ if (live) return { sessionId: id, state: live.status, live: true };
28
+ const headers = await this.ctx.sessionPersistence.list();
29
+ return { sessionId: id, state: headers.some((header) => String(header.id) === id) ? "offline" : "missing", live: false };
30
+ }
31
+ async target(to) {
32
+ const id = String(to);
33
+ // Archive is a durable hide flag: an archived session never receives
34
+ // delivery, whether its agent is still live or only persisted on disk.
35
+ if (new Set(this.ctx.workspaceRegistry?.archivedSessionIds ?? []).has(id)) {
36
+ throw new Error(`session "${id}" is archived and cannot receive messages`);
37
+ }
38
+ const live = this.ctx.agents.get(to);
39
+ if (live) return live;
40
+ let pending = this.resuming.get(id);
41
+ if (!pending) {
42
+ pending = (async () => {
43
+ const provider = this.ctx.typert.lookups.get("agent");
44
+ if (!provider) throw new Error("DSH host agent resolver is unavailable");
45
+ const resolved = await provider.resolve(to);
46
+ if (!resolved) throw new Error(`session "${id}" could not be resumed`);
47
+ return resolved;
48
+ })().then((agent) => {
49
+ return agent;
50
+ }).catch((error) => {
51
+ const raced = this.ctx.agents.get(to);
52
+ if (raced) return raced;
53
+ throw error;
54
+ }).finally(() => this.resuming.delete(id));
55
+ this.resuming.set(id, pending);
56
+ }
57
+ return pending;
58
+ }
59
+ async deliver({ id = crypto.randomUUID(), from, to, text, transport = "local" }) {
60
+ const target = await this.target(to);
61
+ const message = {
62
+ id,
63
+ from: String(from),
64
+ to,
65
+ text,
66
+ createdAt: Date.now(),
67
+ delivered: true,
68
+ transport
69
+ };
70
+ target.followup(createUserMessage({
71
+ content: [{
72
+ type: "text",
73
+ text: `[dsh-bridge ${transport} message ${id} from ${message.from}]\n${text}`
74
+ }],
75
+ source: {
76
+ kind: "plugin",
77
+ plugin: name,
78
+ form: transport
79
+ }
80
+ }));
81
+ this.messages.push(message);
82
+ if (this.messages.length > MAX_RECENT_MESSAGES) {
83
+ this.messages.splice(0, this.messages.length - MAX_RECENT_MESSAGES);
84
+ }
85
+ for (const listener of this.listeners) listener(message);
86
+ return {
87
+ messageId: id,
88
+ from: message.from,
89
+ to: String(to),
90
+ delivered: true
91
+ };
92
+ }
93
+ async send(from, to, text) {
94
+ return this.deliver({ from: from.session.id, to, text });
95
+ }
96
+ async deliverExternal(from, to, text, options = {}) {
97
+ return this.deliver({
98
+ id: options.id,
99
+ from,
100
+ to,
101
+ text,
102
+ transport: options.transport ?? "external"
103
+ });
104
+ }
105
+ subscribe(listener) {
106
+ this.listeners.add(listener);
107
+ return () => this.listeners.delete(listener);
108
+ }
109
+ receive(sessionId, limit) {
110
+ return this.messages.filter((message) => message.to === sessionId).slice(-limit);
111
+ }
112
+ };
113
+ function apply(ctx) {
114
+ const messaging = new LocalSessionMessagingImpl(ctx);
115
+ // `dshBridge` is the public service name. Keep the old accessor for one
116
+ // release so an early local installation does not break on upgrade.
117
+ ctx.accessor("dshBridge", { get: () => messaging });
118
+ ctx.accessor("sessionMessaging", { get: () => messaging });
119
+ ctx.tools.register(defineTool({
120
+ name: "session_list",
121
+ description: "List live DeepSeek Harness sessions in this process. Each returned session is idle or running and can be messaged immediately.",
122
+ parameters: {},
123
+ output: {
124
+ schema: {
125
+ type: "array",
126
+ items: { type: "string" }
127
+ },
128
+ render: (_args, value) => [{
129
+ type: "text",
130
+ text: value.join("\n") || "No live sessions."
131
+ }]
132
+ },
133
+ async execute() {
134
+ return messaging.list().map(String);
135
+ }
136
+ }));
137
+ ctx.tools.register(defineTool({
138
+ name: "session_send",
139
+ description: "Send a message to another DeepSeek Harness session in this host. A persisted offline session is resumed before delivery.",
140
+ parameters: {
141
+ to: {
142
+ type: "string",
143
+ required: true,
144
+ description: "Target session id from session_list."
145
+ },
146
+ text: {
147
+ type: "string",
148
+ required: true,
149
+ description: "Message text."
150
+ }
151
+ },
152
+ output: {
153
+ schema: {
154
+ type: "object",
155
+ additionalProperties: false,
156
+ properties: {
157
+ messageId: {
158
+ type: "string",
159
+ required: true
160
+ },
161
+ from: {
162
+ type: "string",
163
+ required: true
164
+ },
165
+ to: {
166
+ type: "string",
167
+ required: true
168
+ },
169
+ delivered: {
170
+ type: "boolean",
171
+ required: true
172
+ }
173
+ }
174
+ },
175
+ render: (_args, value) => [{
176
+ type: "text",
177
+ text: `Delivered ${value.messageId} to ${value.to}.`
178
+ }]
179
+ },
180
+ async execute(args, exec) {
181
+ if (!exec.agent) throw new Error("session_send requires an owning agent");
182
+ if (args.text.trim() === "") throw new Error("text must not be empty");
183
+ return await messaging.send(exec.agent, SessionId(args.to), args.text);
184
+ }
185
+ }));
186
+ ctx.tools.register(defineTool({
187
+ name: "session_messages",
188
+ description: "Read messages delivered to the current session in this process.",
189
+ parameters: { limit: {
190
+ type: "number",
191
+ description: "Maximum number of messages, default 20."
192
+ } },
193
+ output: {
194
+ schema: {
195
+ type: "array",
196
+ items: {
197
+ type: "object",
198
+ additionalProperties: false,
199
+ properties: {
200
+ id: {
201
+ type: "string",
202
+ required: true
203
+ },
204
+ from: {
205
+ type: "string",
206
+ required: true
207
+ },
208
+ to: {
209
+ type: "string",
210
+ required: true
211
+ },
212
+ text: {
213
+ type: "string",
214
+ required: true
215
+ },
216
+ createdAt: {
217
+ type: "number",
218
+ required: true
219
+ },
220
+ delivered: {
221
+ type: "boolean",
222
+ required: true
223
+ }
224
+ }
225
+ }
226
+ },
227
+ render: (_args, value) => [{
228
+ type: "text",
229
+ text: value.map((message) => `[${message.from}] ${message.text}`).join("\n") || "No messages."
230
+ }]
231
+ },
232
+ async execute(args, exec) {
233
+ if (!exec.agent) throw new Error("session_messages requires an owning agent");
234
+ const limit = args.limit === void 0 ? 20 : Math.max(1, Math.min(100, Math.floor(args.limit)));
235
+ return [...messaging.receive(exec.agent.session.id, limit)];
236
+ }
237
+ }));
238
+ }
239
+ //#endregion
240
+ export { LocalSessionMessagingImpl, apply, inject, name };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Same-process session-to-session messaging.
3
+ *
4
+ * The transport is deliberately process-local. A future relay can implement
5
+ * the same interface without changing the model-facing tools.
6
+ */
7
+ import type { Context } from '@deepseek-ai/cordis';
8
+ import type { Agent } from '@deepseek-ai/dsh-agent';
9
+ import { type SessionId as SessionIdValue } from '@deepseek-ai/dsh-session';
10
+ export declare const name = "dsh-bridge";
11
+ export declare const inject: string[];
12
+ export interface LocalMessage {
13
+ readonly id: string;
14
+ readonly from: string;
15
+ readonly to: SessionIdValue;
16
+ readonly text: string;
17
+ readonly createdAt: number;
18
+ readonly delivered: boolean;
19
+ readonly transport: string;
20
+ }
21
+ export interface SendMessageResult {
22
+ readonly messageId: string;
23
+ readonly from: string;
24
+ readonly to: string;
25
+ readonly delivered: boolean;
26
+ }
27
+ export type SessionRuntimeState = 'idle' | 'running' | 'waking' | 'offline' | 'archived' | 'missing';
28
+ export interface SessionRuntimeStatus {
29
+ readonly sessionId: string;
30
+ readonly state: SessionRuntimeState;
31
+ readonly live: boolean;
32
+ }
33
+ export interface LocalSessionMessaging {
34
+ list(): readonly SessionIdValue[];
35
+ status(sessionId: SessionIdValue): Promise<SessionRuntimeStatus>;
36
+ send(from: Agent, to: SessionIdValue, text: string): Promise<SendMessageResult>;
37
+ deliverExternal(from: string, to: SessionIdValue, text: string, options?: {
38
+ id?: string;
39
+ transport?: string;
40
+ }): Promise<SendMessageResult>;
41
+ subscribe(listener: (message: LocalMessage) => void): () => void;
42
+ receive(sessionId: SessionIdValue, limit: number): readonly LocalMessage[];
43
+ }
44
+ export declare class LocalSessionMessagingImpl implements LocalSessionMessaging {
45
+ constructor(ctx: Context);
46
+ list(): readonly SessionIdValue[];
47
+ status(sessionId: SessionIdValue): Promise<SessionRuntimeStatus>;
48
+ send(from: Agent, to: SessionIdValue, text: string): Promise<SendMessageResult>;
49
+ deliverExternal(from: string, to: SessionIdValue, text: string, options?: { id?: string; transport?: string }): Promise<SendMessageResult>;
50
+ subscribe(listener: (message: LocalMessage) => void): () => void;
51
+ receive(sessionId: SessionIdValue, limit: number): readonly LocalMessage[];
52
+ }
53
+ declare module '@deepseek-ai/cordis' {
54
+ interface Context {
55
+ dshBridge: LocalSessionMessaging;
56
+ sessionMessaging: LocalSessionMessaging;
57
+ }
58
+ }
59
+ export declare function apply(ctx: Context): void;
60
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,114 @@
1
+ import { createUserMessage } from '@deepseek-ai/dsh-llm';
2
+ import { SessionId } from '@deepseek-ai/dsh-session';
3
+ import { defineTool } from '@deepseek-ai/dsh-tools';
4
+ export const name = 'session-messaging';
5
+ export const inject = ['agents', 'tools'];
6
+ class LocalSessionMessagingImpl {
7
+ ctx;
8
+ messages = [];
9
+ constructor(ctx) {
10
+ this.ctx = ctx;
11
+ }
12
+ list() {
13
+ return [...this.ctx.agents.list()].map(agent => agent.session.id);
14
+ }
15
+ send(from, to, text) {
16
+ const target = this.ctx.agents.get(to);
17
+ if (!target)
18
+ throw new Error(`session "${to}" is not live in this process`);
19
+ const id = crypto.randomUUID();
20
+ const message = {
21
+ id,
22
+ from: from.session.id,
23
+ to,
24
+ text,
25
+ createdAt: Date.now(),
26
+ delivered: true,
27
+ };
28
+ this.messages.push(message);
29
+ target.followup(createUserMessage({
30
+ content: [{ type: 'text', text: `[session-message ${id} from ${from.session.id}]\n${text}` }],
31
+ source: { kind: 'plugin', plugin: name, form: 'relay' },
32
+ }));
33
+ return { messageId: id, from: String(from.session.id), to: String(to), delivered: true };
34
+ }
35
+ receive(sessionId, limit) {
36
+ return this.messages.filter(message => message.to === sessionId).slice(-limit);
37
+ }
38
+ }
39
+ export function apply(ctx) {
40
+ const messaging = new LocalSessionMessagingImpl(ctx);
41
+ ctx.accessor('sessionMessaging', { get: () => messaging });
42
+ ctx.tools.register(defineTool({
43
+ name: 'session_list',
44
+ description: 'List live DeepSeek Harness sessions in this process.',
45
+ parameters: {},
46
+ output: {
47
+ schema: { type: 'array', items: { type: 'string' } },
48
+ render: (_args, value) => [{ type: 'text', text: value.join('\n') || 'No live sessions.' }],
49
+ },
50
+ async execute() {
51
+ return messaging.list().map(String);
52
+ },
53
+ }));
54
+ ctx.tools.register(defineTool({
55
+ name: 'session_send',
56
+ description: 'Send a message to another live DeepSeek Harness session in this process.',
57
+ parameters: {
58
+ to: { type: 'string', required: true, description: 'Target session id from session_list.' },
59
+ text: { type: 'string', required: true, description: 'Message text.' },
60
+ },
61
+ output: {
62
+ schema: {
63
+ type: 'object',
64
+ additionalProperties: false,
65
+ properties: {
66
+ messageId: { type: 'string', required: true },
67
+ from: { type: 'string', required: true },
68
+ to: { type: 'string', required: true },
69
+ delivered: { type: 'boolean', required: true },
70
+ },
71
+ },
72
+ render: (_args, value) => [{ type: 'text', text: `Delivered ${value.messageId} to ${value.to}.` }],
73
+ },
74
+ async execute(args, exec) {
75
+ if (!exec.agent)
76
+ throw new Error('session_send requires an owning agent');
77
+ if (args.text.trim() === '')
78
+ throw new Error('text must not be empty');
79
+ return messaging.send(exec.agent, SessionId(args.to), args.text);
80
+ },
81
+ }));
82
+ ctx.tools.register(defineTool({
83
+ name: 'session_messages',
84
+ description: 'Read messages delivered to the current session in this process.',
85
+ parameters: {
86
+ limit: { type: 'number', description: 'Maximum number of messages, default 20.' },
87
+ },
88
+ output: {
89
+ schema: {
90
+ type: 'array',
91
+ items: {
92
+ type: 'object',
93
+ additionalProperties: false,
94
+ properties: {
95
+ id: { type: 'string', required: true },
96
+ from: { type: 'string', required: true },
97
+ to: { type: 'string', required: true },
98
+ text: { type: 'string', required: true },
99
+ createdAt: { type: 'number', required: true },
100
+ delivered: { type: 'boolean', required: true },
101
+ },
102
+ },
103
+ },
104
+ render: (_args, value) => [{ type: 'text', text: value.map(message => `[${message.from}] ${message.text}`).join('\n') || 'No messages.' }],
105
+ },
106
+ async execute(args, exec) {
107
+ if (!exec.agent)
108
+ throw new Error('session_messages requires an owning agent');
109
+ const limit = args.limit === undefined ? 20 : Math.max(1, Math.min(100, Math.floor(args.limit)));
110
+ return [...messaging.receive(exec.agent.session.id, limit)];
111
+ },
112
+ }));
113
+ }
114
+ //# sourceMappingURL=index.js.map
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "dsh-bridge",
3
+ "description": "Local session messaging and event bridge for DeepSeek Harness",
4
+ "version": "0.1.0-rc.10",
5
+ "publishConfig": { "access": "public", "tag": "next" },
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/baixianger/dsh-bridge.git"
9
+ },
10
+ "type": "module",
11
+ "scripts": {
12
+ "check": "node --check lib/index.js && npm pack --dry-run",
13
+ "test": "node --test test/**/*.test.mjs"
14
+ },
15
+ "main": "lib/index.js",
16
+ "types": "lib/types/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./lib/types/index.d.ts",
20
+ "default": "./lib/index.js"
21
+ }
22
+ },
23
+ "dsh": {
24
+ "bundle": {
25
+ "patch": "./cordis.patch.yml"
26
+ }
27
+ },
28
+ "files": ["lib/index.js", "lib/types/**/*.js", "lib/types/**/*.d.ts", "cordis.patch.yml", "README.md"],
29
+ "license": "MIT",
30
+ "peerDependencies": {
31
+ "@deepseek-ai/cordis": ">=0.1.0-rc.5",
32
+ "@deepseek-ai/dsh-agent": ">=0.1.0-rc.5",
33
+ "@deepseek-ai/dsh-llm": ">=0.1.0-rc.5",
34
+ "@deepseek-ai/dsh-session": ">=0.1.0-rc.5",
35
+ "@deepseek-ai/dsh-tools": ">=0.1.0-rc.5"
36
+ },
37
+ "devDependencies": {
38
+ "@deepseek-ai/cordis": ">=0.1.0-rc.5",
39
+ "@deepseek-ai/dsh-agent": ">=0.1.0-rc.5",
40
+ "@deepseek-ai/dsh-llm": ">=0.1.0-rc.5",
41
+ "@deepseek-ai/dsh-session": ">=0.1.0-rc.5",
42
+ "@deepseek-ai/dsh-tools": ">=0.1.0-rc.5"
43
+ }
44
+ }