@shendeguize/dsh-agent-sidecar 0.1.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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +167 -0
  3. package/cordis.patch.yml +10 -0
  4. package/lib/client.js +8062 -0
  5. package/lib/client.js.map +1 -0
  6. package/lib/index.d.ts +396 -0
  7. package/lib/index.js +4166 -0
  8. package/package.json +101 -0
  9. package/src/analysis.ts +782 -0
  10. package/src/bridge.ts +841 -0
  11. package/src/client/analysis/AnalysisPanel.tsx +191 -0
  12. package/src/client/analysis/analysis.module.css +183 -0
  13. package/src/client/analysis-glue.ts +331 -0
  14. package/src/client/api.ts +380 -0
  15. package/src/client/board/Board.tsx +214 -0
  16. package/src/client/board/board.module.css +302 -0
  17. package/src/client/board/logic.ts +556 -0
  18. package/src/client/board/project-view-logic.ts +361 -0
  19. package/src/client/board/project-view.module.css +307 -0
  20. package/src/client/board/project-view.tsx +189 -0
  21. package/src/client/board/strings.ts +112 -0
  22. package/src/client/commands.ts +484 -0
  23. package/src/client/controller.ts +360 -0
  24. package/src/client/css-modules.d.ts +11 -0
  25. package/src/client/detail/SessionDetail.tsx +270 -0
  26. package/src/client/detail/detail.module.css +433 -0
  27. package/src/client/detail/logic.ts +779 -0
  28. package/src/client/detail/strings.ts +98 -0
  29. package/src/client/detail/transport.ts +175 -0
  30. package/src/client/detail-glue.ts +397 -0
  31. package/src/client/detail-view.module.css +79 -0
  32. package/src/client/detail-view.tsx +233 -0
  33. package/src/client/dsh-tools/LineageTree.tsx +210 -0
  34. package/src/client/dsh-tools/SearchPanel.tsx +169 -0
  35. package/src/client/dsh-tools/dsh-tools.module.css +374 -0
  36. package/src/client/dsh-tools/logic.ts +596 -0
  37. package/src/client/dsh-tools/strings.ts +90 -0
  38. package/src/client/index.ts +315 -0
  39. package/src/client/inject/InjectPanel.tsx +482 -0
  40. package/src/client/inject/inject.module.css +446 -0
  41. package/src/client/inject/logic.ts +516 -0
  42. package/src/client/inject/overlay.module.css +22 -0
  43. package/src/client/inject-glue.ts +171 -0
  44. package/src/client/locales/command.ts +48 -0
  45. package/src/client/locales/en.ts +385 -0
  46. package/src/client/locales/index.ts +123 -0
  47. package/src/client/locales/zh.ts +402 -0
  48. package/src/client/m3-transport.ts +151 -0
  49. package/src/client/mount.tsx +307 -0
  50. package/src/client/project-glue.ts +134 -0
  51. package/src/client/search-glue.ts +143 -0
  52. package/src/client/settings-card.module.css +359 -0
  53. package/src/client/settings-card.tsx +565 -0
  54. package/src/client/settings-glue.ts +130 -0
  55. package/src/client/sidebar-tab.tsx +494 -0
  56. package/src/client/sse.ts +366 -0
  57. package/src/client/widget.tsx +80 -0
  58. package/src/config.ts +193 -0
  59. package/src/dsh-inject.ts +240 -0
  60. package/src/fusion.ts +988 -0
  61. package/src/guard.ts +274 -0
  62. package/src/index.ts +950 -0
  63. package/src/inject-gateway.ts +574 -0
  64. package/src/routes.ts +1133 -0
  65. package/src/send-cli.ts +340 -0
  66. package/src/session-store.ts +184 -0
  67. package/src/skills-provider.ts +293 -0
  68. package/src/supervisor.ts +463 -0
@@ -0,0 +1,240 @@
1
+ /**
2
+ * dsh in-process injection executor — injection path one of design §4.d
3
+ * (.local/tasks/make_dsh_mode/design/dsh_plugin_design.md): native dsh
4
+ * sessions are reached through the `ctx.agents` registry, closing the
5
+ * sidecar send `unsupported_dsh` gap.
6
+ *
7
+ * API facts verified against the installed SDK
8
+ * (`@deepseek-ai/dsh-agent@0.1.1-rc.2` d.ts, authoritative over the design
9
+ * sketch):
10
+ *
11
+ * - `ctx.agents` is `AgentRegistry` (lib/types/index.d.ts:28):
12
+ * `get(id): Agent | undefined` (:349) and
13
+ * `resume({resumeSessionId}): Promise<AgentHandle>` (:296) with
14
+ * `AgentHandle = { agent; dispose() }` (:155-158).
15
+ * - `Agent.followup(message: UserMessage): void` and
16
+ * `Agent.steer(message: UserMessage): void` are SYNCHRONOUS inbox splices
17
+ * (runtime-types.d.ts:115/:123) — the design sketch's `await` is a
18
+ * deviation. A sync return means the message entered the live inbox
19
+ * (delivered); a sync throw is the only call-site failure face.
20
+ * - `UserMessage` = `{ id, role: 'user', content: ContentBlock[], source }`
21
+ * (dsh-llm message.d.ts:120-133); text content is `[{type:'text',text}]`
22
+ * (types.d.ts:39-42, F11) and plugin attribution is
23
+ * `source: { kind: 'plugin', plugin: <name> }` (message.d.ts:98-101).
24
+ * - resume is NOT idempotent: resuming a live session throws
25
+ * `cannot prepare session "<id>" while it is live`
26
+ * (session-persistence coordinator), and resume rejects when no
27
+ * persistence backend is configured — so the executor MUST `get` first
28
+ * and only resume a miss. persistence prepare may also retry unboundedly
29
+ * under concurrent writers, hence the bounded resume wait here.
30
+ *
31
+ * queue → `followup` (own next turn), steer → `steer` (nearest step),
32
+ * matching dsh's own `session.prompt` mode vocabulary.
33
+ *
34
+ * Error vocabulary is normalized with path two on the shared subset
35
+ * `session_not_found | executor_error | timeout`; dsh-native error text
36
+ * only travels in `detail`. Log lines never carry the message body — only
37
+ * byte size and a sha256 prefix (S8).
38
+ *
39
+ * Pure DI: no cordis/dsh imports. {@link AgentsServiceFace} is a minimal
40
+ * structural face extracted from the d.ts; the real `AgentRegistry`
41
+ * satisfies it directly (method-syntax members keep parameter checks
42
+ * bivariant, so the SDK's branded `SessionId` / wider `UserMessage`
43
+ * signatures remain assignable).
44
+ *
45
+ * @module
46
+ */
47
+
48
+ import { createHash } from 'node:crypto'
49
+
50
+ import type { InjectExecutionRequest, InjectExecutor, InjectResult } from './inject-gateway.ts'
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Minimal structural faces over the dsh-agent SDK.
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /** The exact message this executor constructs (F11: content-block array). */
57
+ export interface DshInjectMessage {
58
+ /** Plain-string form of the SDK's branded `MessageId`; unique per request. */
59
+ readonly id: string
60
+ readonly role: 'user'
61
+ readonly content: readonly [{ readonly type: 'text'; readonly text: string }]
62
+ /** Plugin attribution (message.d.ts:98-101). */
63
+ readonly source: { readonly kind: 'plugin'; readonly plugin: string }
64
+ }
65
+
66
+ /**
67
+ * Widened message parameter face: the SDK's `UserMessage` is assignable to
68
+ * this shape, which keeps `Agent`'s method signatures structurally
69
+ * compatible with {@link DshAgentFace} without importing SDK types.
70
+ */
71
+ export interface DshUserMessageFace {
72
+ readonly id: string
73
+ readonly role: 'user'
74
+ readonly content: ReadonlyArray<{ readonly type: string }>
75
+ readonly source: { readonly kind: string }
76
+ }
77
+
78
+ /** Live-agent face: the two injection entry points (runtime-types.d.ts:115/:123). */
79
+ export interface DshAgentFace {
80
+ followup(message: DshUserMessageFace): void
81
+ steer(message: DshUserMessageFace): void
82
+ }
83
+
84
+ /** `AgentHandle` face (index.d.ts:155-158). `dispose` is deliberately absent:
85
+ * the executor never tears down an agent it resumed — disposal would unload
86
+ * the session and cancel the just-queued work. */
87
+ export interface DshAgentHandleFace {
88
+ readonly agent: DshAgentFace
89
+ }
90
+
91
+ /** Minimal `ctx.agents` (`AgentRegistry`) face: locate + resume. */
92
+ export interface AgentsServiceFace {
93
+ /** Live lookup (index.d.ts:349); undefined = session not loaded. */
94
+ get(sessionId: string): DshAgentFace | undefined
95
+ /** Load a persisted session and start an agent on it (index.d.ts:296). */
96
+ resume(options: { readonly resumeSessionId: string }): Promise<DshAgentHandleFace>
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Logging seam (same shape as send-cli's; never receives the message body).
101
+ // ---------------------------------------------------------------------------
102
+
103
+ export type DshInjectLogLevel = 'debug' | 'info' | 'warn' | 'error'
104
+
105
+ export type DshInjectLog = (
106
+ level: DshInjectLogLevel,
107
+ msg: string,
108
+ meta?: Record<string, unknown>,
109
+ ) => void
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Tunables.
113
+ // ---------------------------------------------------------------------------
114
+
115
+ /** Default `source.plugin` attribution value. */
116
+ export const DEFAULT_PLUGIN_NAME = 'agent-sidecar'
117
+ /**
118
+ * Bounded wait for `agents.resume`: persistence prepare retries unboundedly
119
+ * under concurrent external writers, so an unloaded-session resume is the
120
+ * one async face of this otherwise in-process path.
121
+ */
122
+ export const DEFAULT_RESUME_TIMEOUT_MS = 30_000
123
+
124
+ /** Sha256 hex prefix length recorded in logs (matches inject-gateway). */
125
+ const SHA_LOG_CHARS = 12
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Internals.
129
+ // ---------------------------------------------------------------------------
130
+
131
+ function describeError(error: unknown): string {
132
+ return error instanceof Error ? error.message : String(error)
133
+ }
134
+
135
+ /** Race a resume against the bounded wait; null = timed out (still pending). */
136
+ async function resumeWithin(
137
+ resume: Promise<DshAgentHandleFace>,
138
+ timeoutMs: number,
139
+ ): Promise<DshAgentHandleFace | null> {
140
+ let timer: ReturnType<typeof setTimeout> | undefined
141
+ try {
142
+ return await Promise.race([
143
+ resume,
144
+ new Promise<null>((resolve) => {
145
+ timer = setTimeout(() => resolve(null), timeoutMs)
146
+ }),
147
+ ])
148
+ } finally {
149
+ if (timer !== undefined) clearTimeout(timer)
150
+ }
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Executor.
155
+ // ---------------------------------------------------------------------------
156
+
157
+ export function createDshInjectExecutor(deps: {
158
+ agents: AgentsServiceFace
159
+ log?: DshInjectLog
160
+ pluginName?: string
161
+ /** Bounded resume wait override; default {@link DEFAULT_RESUME_TIMEOUT_MS}. */
162
+ resumeTimeoutMs?: number
163
+ }): InjectExecutor {
164
+ const pluginName = deps.pluginName ?? DEFAULT_PLUGIN_NAME
165
+ const log: DshInjectLog = deps.log ?? (() => {})
166
+ const resumeTimeoutMs = deps.resumeTimeoutMs ?? DEFAULT_RESUME_TIMEOUT_MS
167
+
168
+ return {
169
+ kind: 'dsh',
170
+ async execute(req: InjectExecutionRequest): Promise<InjectResult> {
171
+ const sessionId = req.target.sessionId
172
+ const messageBytes = Buffer.byteLength(req.message, 'utf8')
173
+ const messageSha12 = createHash('sha256')
174
+ .update(req.message, 'utf8')
175
+ .digest('hex')
176
+ .slice(0, SHA_LOG_CHARS)
177
+ const baseMeta = {
178
+ requestId: req.requestId,
179
+ sessionId,
180
+ mode: req.mode,
181
+ messageBytes,
182
+ messageSha12,
183
+ }
184
+
185
+ // Locate: loaded → inject directly; resume throws on a live session
186
+ // ("cannot prepare ... while it is live"), so `get` must win first.
187
+ let agent = deps.agents.get(sessionId)
188
+ const resumed = agent === undefined
189
+
190
+ if (agent === undefined) {
191
+ log('debug', 'dsh session not loaded; resuming', baseMeta)
192
+ let handle: DshAgentHandleFace | null
193
+ try {
194
+ handle = await resumeWithin(
195
+ deps.agents.resume({ resumeSessionId: sessionId }),
196
+ resumeTimeoutMs,
197
+ )
198
+ } catch (error) {
199
+ const detail = describeError(error)
200
+ log('warn', 'dsh resume failed', { ...baseMeta, error: detail })
201
+ return { outcome: 'failed', errorCode: 'session_not_found', detail }
202
+ }
203
+ if (handle === null) {
204
+ // The resume is still pending; nothing was injected, so 'failed'
205
+ // is honest (unlike path two's post-dispatch 'unknown').
206
+ log('warn', 'dsh resume timed out', { ...baseMeta, resumeTimeoutMs })
207
+ return {
208
+ outcome: 'failed',
209
+ errorCode: 'timeout',
210
+ detail: `resume did not settle within ${resumeTimeoutMs}ms`,
211
+ }
212
+ }
213
+ // The handle's dispose() capability is intentionally dropped: the
214
+ // resumed agent must stay live to run the queued/steered turn.
215
+ agent = handle.agent
216
+ }
217
+
218
+ const message: DshInjectMessage = {
219
+ id: `${pluginName}-${req.requestId}`,
220
+ role: 'user',
221
+ content: [{ type: 'text', text: req.message }],
222
+ source: { kind: 'plugin', plugin: pluginName },
223
+ }
224
+
225
+ // queue → followup (own next turn), steer → steer (nearest step).
226
+ // Both are synchronous inbox splices: sync return = delivered.
227
+ try {
228
+ if (req.mode === 'steer') agent.steer(message)
229
+ else agent.followup(message)
230
+ } catch (error) {
231
+ const detail = describeError(error)
232
+ log('warn', 'dsh injection call threw', { ...baseMeta, resumed, error: detail })
233
+ return { outcome: 'failed', errorCode: 'executor_error', detail }
234
+ }
235
+
236
+ log('info', 'dsh injection delivered', { ...baseMeta, resumed })
237
+ return { outcome: 'delivered' }
238
+ },
239
+ }
240
+ }