@logictan/dsh-desktop-agent 0.1.1 → 0.1.3

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 CHANGED
@@ -29,8 +29,14 @@ describing a picture it never received — and answering confidently about it.
29
29
 
30
30
  ## Requirements
31
31
 
32
- The profile must mount the Cua Driver computer-use provider; the plugin dispatches
33
- to the `cua_driver_native__*` tools it publishes and has no driver of its own.
32
+ None beyond installing this plugin. It publishes the `cua_driver_native__*` tools
33
+ itself, from the Cua Driver native SDK it depends on, and dispatches to them.
34
+
35
+ A profile that still mounts the old
36
+ `@deepseek-ai/dsh-experimental-computer-use-cua-driver-native` provider must drop
37
+ that row and its `@deepseek-ai/dsh-computer-use` dependency first: both would
38
+ publish the same tool names, and the second registration is refused with
39
+ `tool "cua_driver_native__click" is already registered`.
34
40
 
35
41
  ## Settings
36
42
 
package/cordis.patch.yml CHANGED
@@ -1,8 +1,9 @@
1
1
  # dsh-desktop-agent bundle patch layer.
2
2
  #
3
3
  # One bare plugin row. The package ships two halves — the host half
4
- # (exports ".") registers the `desktop_agent` model tool and exports the
5
- # `Config` schema the Plugins page derives its form from, the browser half
4
+ # (exports ".") registers the `desktop_agent` model tool, publishes the
5
+ # `cua_driver_native__*` driver tools from `src/driver.js`, and exports the
6
+ # `Config` schema the Plugins page derives its form from; the browser half
6
7
  # (exports "./client") registers the Plugins-page configuration card.
7
8
  #
8
9
  # The row must stay a BARE package name: the web plugin table locates each
@@ -13,6 +14,11 @@
13
14
  # Row id `desktop-agent` equals the host half's `export const name` in
14
15
  # src/index.js. It is checked by hand against every other row id in this
15
16
  # repository; `aggregate.mjs --check` does not validate id uniqueness.
17
+ #
18
+ # This row replaces the two hand-written rows a profile used to need
19
+ # (`computer-use` + `computer-use-cua-driver-native`): the driver runtime now
20
+ # lives in this package, so mounting it twice would collide at tool
21
+ # registration.
16
22
  - insert:
17
23
  - id: desktop-agent
18
24
  name: '@logictan/dsh-desktop-agent'
package/lib/driver.js ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * The Cua Driver native runtime — the driver half.
3
+ *
4
+ * Absorbed from `@deepseek-ai/dsh-experimental-computer-use-cua-driver-native`,
5
+ * which the profile used to mount as its own loader row. The desktop agent drives
6
+ * the desktop exclusively by dispatching to the driver's `cua_driver_native__*`
7
+ * tools through the tool registry, so those tools have to be published by
8
+ * whoever owns the native handle; keeping that ownership in this package makes
9
+ * the plugin self-contained instead of leaving it inert without a second row.
10
+ *
11
+ * What the absorption deliberately drops: the `computerUse` registration slot.
12
+ * That slot existed to keep two computer-use providers from claiming the same
13
+ * desktop, and the plugin is now the only publisher of these tools — a profile
14
+ * that still mounts the old provider collides at `tools.register` with
15
+ * `tool "cua_driver_native__click" is already registered`, which is the honest
16
+ * signal that the old row must go.
17
+ *
18
+ * @module @logictan/dsh-desktop-agent/driver
19
+ */
20
+ import { createMcpToolDefinition } from '@deepseek-ai/dsh-mcp-client';
21
+
22
+ /** Every published tool name carries this prefix; the loop's dispatch names depend on it. */
23
+ export const TOOL_PREFIX = 'cua_driver_native__';
24
+
25
+ /** DeepSeek's function-name alphabet and maximum length are protocol constants. */
26
+ const TOOL_NAME = /^[A-Za-z0-9_-]{1,64}$/u;
27
+
28
+ /** The prompt section this runtime owns. */
29
+ const SECTION_NAME = 'computer-use:cua-driver-native';
30
+
31
+ const GUIDANCE = `Cua Driver native computer-use tools operate the host desktop. Discover the exact app and window, then get a fresh window snapshot before acting. Use element_token from that snapshot, or coordinates from its screenshot. A new snapshot of that window invalidates its earlier element tokens. Select either target or the legacy pid/window_id fields; do not combine them.
32
+
33
+ Prefer background delivery. A refusal does not authorize a foreground retry. Verify the requested outcome from fresh state after an action; a delivered click alone does not prove the outcome. After cancellation, inspect current state before retrying because completed input is not rolled back. Other sessions and applications may change the same desktop.
34
+
35
+ On macOS, cursor-overlay operations may return facility_unavailable even when screenshots and input work.`;
36
+
37
+ /**
38
+ * Load the native SDK module.
39
+ *
40
+ * The one external boundary this plugin cannot validate away: the driver ships
41
+ * as per-platform native binaries, so the module is imported at the point of use
42
+ * rather than at module load, and a missing binary fails the mount with the
43
+ * resolver's own message instead of a module-scope import error.
44
+ *
45
+ * @returns the SDK module namespace.
46
+ */
47
+ function loadNativeSdk() {
48
+ return import('@trycua/cua-driver');
49
+ }
50
+
51
+ /**
52
+ * Read the tool catalog out of the driver's JSON.
53
+ *
54
+ * The SDK marshals this from a native process, so it is a real untrusted
55
+ * boundary: a shape change must be reported here, not carried into tool
56
+ * registration where it would surface as an unexplained schema error.
57
+ *
58
+ * @param value - the parsed catalog payload.
59
+ * @returns the catalog entries.
60
+ * @throws {Error} when the payload is not a catalog.
61
+ */
62
+ function readCatalog(value) {
63
+ const tools = value?.tools;
64
+ if (!Array.isArray(tools)) throw new Error('Cua Driver returned no tool catalog.');
65
+ return tools;
66
+ }
67
+
68
+ /**
69
+ * Mount the native runtime on `ctx`.
70
+ *
71
+ * The effect is registered before the first `await` so that a failure anywhere
72
+ * after the native handle exists still releases it, and so that unloading the
73
+ * plugin mid-startup aborts the in-flight discovery call instead of waiting for
74
+ * it to finish.
75
+ *
76
+ * @param ctx - the plugin context, providing `tools` and optionally `systemPrompt`.
77
+ * @param loadSdk - the SDK loader; overridden by tests, which cannot import the native binaries.
78
+ * @returns the effect disposer that tears the runtime down.
79
+ * @throws {Error} when the SDK cannot be loaded, the catalog is unusable, or a registration is refused.
80
+ */
81
+ export async function mountDriver(ctx, loadSdk = loadNativeSdk) {
82
+ const lifetime = new AbortController();
83
+ const pending = new Set();
84
+ let driver;
85
+
86
+ const dispose = ctx.effect(
87
+ () => async () => {
88
+ lifetime.abort();
89
+ // Shutting the native handle down under an in-flight call would release
90
+ // memory the call still reads, so settlement comes first.
91
+ await Promise.allSettled(pending);
92
+ if (driver !== undefined) {
93
+ await driver.shutdown();
94
+ driver.uniffiDestroy();
95
+ }
96
+ },
97
+ 'desktop-agent: cua driver runtime',
98
+ );
99
+
100
+ try {
101
+ const { CuaDriver } = await loadSdk();
102
+ lifetime.signal.throwIfAborted();
103
+ // The generated constructor returns its class with an owned binding handle,
104
+ // but declares only CuaDriverLike, which omits uniffiDestroy().
105
+ driver = CuaDriver.create(undefined);
106
+ await publishTools(ctx, driver, lifetime, pending);
107
+ } catch (error) {
108
+ await dispose();
109
+ throw error;
110
+ }
111
+
112
+ return dispose;
113
+ }
114
+
115
+ /**
116
+ * Publish one model tool per catalog entry and the usage guidance section.
117
+ *
118
+ * @param ctx - the plugin context.
119
+ * @param driver - the live native handle.
120
+ * @param lifetime - the runtime's abort source.
121
+ * @param pending - the in-flight native calls the teardown waits for.
122
+ */
123
+ async function publishTools(ctx, driver, lifetime, pending) {
124
+ const tools = ctx.get('tools');
125
+ const catalog = readCatalog(JSON.parse(await driver.listToolsJson({ signal: lifetime.signal })));
126
+ lifetime.signal.throwIfAborted();
127
+
128
+ const names = new Set();
129
+ for (const tool of catalog) {
130
+ if (typeof tool?.name !== 'string' || tool.name === '') {
131
+ throw new Error('Cua Driver listed a tool without a name.');
132
+ }
133
+ const publicName = TOOL_PREFIX + tool.name;
134
+ if (!TOOL_NAME.test(publicName)) {
135
+ throw new Error(`Cua Driver tool "${tool.name}" exceeds the supported function-name format`);
136
+ }
137
+ if (names.has(publicName)) throw new Error(`Cua Driver listed tool "${tool.name}" more than once`);
138
+ names.add(publicName);
139
+
140
+ tools.register(createMcpToolDefinition(ctx, {
141
+ name: publicName,
142
+ rawName: tool.name,
143
+ description: tool.description ?? '',
144
+ inputSchema: tool.inputSchema,
145
+ outputSchema: tool.outputSchema,
146
+ async call(args, execution) {
147
+ // Two independent cancellations: the caller giving up, and the plugin
148
+ // being unloaded. The native call must observe both.
149
+ const combined = AbortSignal.any([execution.signal, lifetime.signal]);
150
+ combined.throwIfAborted();
151
+ const operation = driver.callTool(tool.name, JSON.stringify(args), { signal: combined });
152
+ pending.add(operation);
153
+ try {
154
+ const result = await operation;
155
+ combined.throwIfAborted();
156
+ return JSON.parse(result.rawJson);
157
+ } finally {
158
+ pending.delete(operation);
159
+ }
160
+ },
161
+ }));
162
+ }
163
+
164
+ const systemPrompt = ctx.get('systemPrompt');
165
+ if (systemPrompt === null || systemPrompt === undefined) {
166
+ ctx.logger?.warn?.('desktop-agent: the systemPrompt service is unavailable; the Cua Driver guidance was not registered.');
167
+ return;
168
+ }
169
+ ctx.effect(
170
+ () => systemPrompt.section({
171
+ name: SECTION_NAME,
172
+ order: systemPrompt.getSectionOrder('TOOL_COMPUTER_USE'),
173
+ text: GUIDANCE,
174
+ }),
175
+ 'desktop-agent: cua driver guidance',
176
+ );
177
+ }
package/lib/index.js CHANGED
@@ -6,10 +6,11 @@
6
6
  * card reads its model list from.
7
7
  *
8
8
  * The plugin owns no execution engine of its own. Every action is dispatched to
9
- * the `cua_driver_native__*` tools the profile already mounts, through the tool
10
- * registry's own `execute` pipeline — so a call this plugin makes is subject to
11
- * the same guards, timeouts, and cancellation as one the model made directly, and
12
- * the driver's own coordinate and delivery contracts are the only ones in play.
9
+ * the `cua_driver_native__*` tools this package publishes — see `./driver.js` —
10
+ * through the tool registry's own `execute` pipeline, so a call this plugin makes
11
+ * is subject to the same guards, timeouts, and cancellation as one the model made
12
+ * directly, and the driver's own coordinate and delivery contracts are the only
13
+ * ones in play.
13
14
  *
14
15
  * Configuration arrives as `apply`'s second argument. As of the 0.1.7-alpha.1
15
16
  * settings redesign the plugin's own `Config` schema is the only config surface —
@@ -42,6 +43,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools';
42
43
  import { ToolCallId } from '@deepseek-ai/dsh-llm';
43
44
 
44
45
  import { ENTRY_ID, resolveVisionRoute } from './config.js';
46
+ import { mountDriver } from './driver.js';
45
47
  import { run } from './loop.js';
46
48
  import { structured } from './observe.js';
47
49
  import { makeRoutes } from './routes.js';
@@ -61,12 +63,16 @@ export const name = ENTRY_ID;
61
63
  export const inject = [];
62
64
 
63
65
  /**
64
- * Register the `desktop_agent` tool and the vision-catalog route.
66
+ * Register the `desktop_agent` tool, the vision-catalog route, and the Cua
67
+ * Driver runtime the tool dispatches to.
65
68
  *
66
69
  * @param ctx - the plugin context.
67
70
  * @param config - one live reference per {@link Config} field.
71
+ * @param loadSdk - the native SDK loader, forwarded to {@link mountDriver}; tests
72
+ * pass one because the driver ships native binaries that are not installed in
73
+ * this workspace, and activation must not depend on a binary the test cannot load.
68
74
  */
69
- export function apply(ctx, config) {
75
+ export async function apply(ctx, config, loadSdk) {
70
76
  const tools = ctx.get('tools');
71
77
  if (tools === null || tools === undefined || typeof tools !== 'object') {
72
78
  ctx.logger?.warn?.('desktop_agent: the tools service is unavailable; the tool was not registered.');
@@ -146,7 +152,7 @@ export function apply(ctx, config) {
146
152
  'Resolves the target application, then runs an observation/decision loop: each step captures the window, ' +
147
153
  'asks the configured model what to do, and performs the click, key, or text entry it chose. ' +
148
154
  'Works on ordinary applications and on games that expose no usable accessibility tree, because it reads the ' +
149
- 'picture rather than the element table. Requires the computer-use Cua Driver tools to be mounted. ' +
155
+ 'picture rather than the element table. ' +
150
156
  'Note that a covered window cannot be driven, and a game that filters injected input needs foreground ' +
151
157
  'delivery — the plugin settings control that. This tool waits for the run to finish and returns a structured ' +
152
158
  'trace of every step.',
@@ -215,6 +221,10 @@ export function apply(ctx, config) {
215
221
  'desktop-agent: vision-model catalog route',
216
222
  );
217
223
  });
224
+
225
+ // Last, so that a driver that cannot start costs this plugin nothing else: a
226
+ // rejected mount rolls back the tool and route registrations above with it.
227
+ await mountDriver(ctx, loadSdk);
218
228
  }
219
229
 
220
230
  /**
@@ -235,7 +245,12 @@ async function resolveTarget(input) {
235
245
 
236
246
  const value = await dispatch('cua_driver_native__list_windows', {}, signal);
237
247
  const payload = structured(value, 'list_windows');
238
- const windows = Array.isArray(payload.windows) ? payload.windows : [];
248
+ // A payload that parses but carries no window array is a driver contract
249
+ // failure, not an empty desktop. Flattening it to [] would report the two as
250
+ // the same sentence, and the driver is a third-party boundary whose response
251
+ // shape this plugin does not control.
252
+ if (!Array.isArray(payload.windows)) throw new Error('desktop_agent: list_windows returned no window array.');
253
+ const windows = payload.windows;
239
254
  if (windows.length === 0) throw new Error('desktop_agent: the driver listed no windows.');
240
255
 
241
256
  if (args.windowId !== undefined) {
@@ -262,20 +277,41 @@ async function resolveTarget(input) {
262
277
  }
263
278
 
264
279
  const needle = args.app.toLowerCase();
265
- const matches = onScreen.filter(
266
- (window) =>
267
- String(window.app_name ?? '').toLowerCase().includes(needle) ||
268
- String(window.title ?? '').toLowerCase().includes(needle),
269
- );
280
+ const matches = onScreen.filter((window) => matchesApp(window, needle));
270
281
  if (matches.length === 0) {
282
+ // An app that is running but has no window on screen is a different fact
283
+ // from an app that is not running, and only the second one suggests
284
+ // launching it. The windowId path already tells them apart.
285
+ if (windows.some((window) => matchesApp(window, needle))) {
286
+ throw new Error(
287
+ 'desktop_agent: "' + args.app + '" is open, but no window of it is on screen, so it cannot be captured or acted on.',
288
+ );
289
+ }
271
290
  const names = [...new Set(onScreen.map((window) => window.app_name).filter(Boolean))];
272
291
  throw new Error(
273
- 'desktop_agent: no on-screen window matches "' + args.app + '". Open windows: ' + (names.join(', ') || '(none)') + '.',
292
+ 'desktop_agent: no on-screen window matches "' + args.app + '". On-screen apps: ' + (names.join(', ') || '(none)') + '.',
274
293
  );
275
294
  }
276
295
  return targetOf(bestMatch(matches));
277
296
  }
278
297
 
298
+ /**
299
+ * Whether a window answers to an app argument.
300
+ *
301
+ * The needle is matched against the application name and the window title,
302
+ * because both are things a caller can reasonably name.
303
+ *
304
+ * @param window - a window from the driver's list.
305
+ * @param needle - the app argument, lower-cased.
306
+ * @returns whether the window matches.
307
+ */
308
+ function matchesApp(window, needle) {
309
+ return (
310
+ String(window.app_name ?? '').toLowerCase().includes(needle) ||
311
+ String(window.title ?? '').toLowerCase().includes(needle)
312
+ );
313
+ }
314
+
279
315
  /**
280
316
  * Choose the window to drive from the candidates an app argument matched.
281
317
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logictan/dsh-desktop-agent",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "DSH desktop agent: a native-vision decision loop that drives the host desktop through the Cua Driver tools. DSH 桌面 Agent 子插件。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -55,6 +55,7 @@
55
55
  "peerDependencies": {
56
56
  "@deepseek-ai/cordis": "^4.0.2",
57
57
  "@deepseek-ai/dsh-llm": "^0.1.6-alpha.2",
58
+ "@deepseek-ai/dsh-system-prompt": "^0.1.6-alpha.2",
58
59
  "@deepseek-ai/dsh-tools": "^0.1.6-alpha.2",
59
60
  "@deepseek-ai/schemastery": "^3.18.3"
60
61
  },
@@ -65,6 +66,9 @@
65
66
  "@deepseek-ai/dsh-llm": {
66
67
  "optional": true
67
68
  },
69
+ "@deepseek-ai/dsh-system-prompt": {
70
+ "optional": true
71
+ },
68
72
  "@deepseek-ai/dsh-tools": {
69
73
  "optional": true
70
74
  },
@@ -72,9 +76,15 @@
72
76
  "optional": true
73
77
  }
74
78
  },
79
+ "dependencies": {
80
+ "@deepseek-ai/dsh-mcp-client": "^0.1.6-alpha.2",
81
+ "@trycua/cua-driver": "0.28.0"
82
+ },
75
83
  "devDependencies": {
76
84
  "@deepseek-ai/cordis": "^4.0.2",
77
85
  "@deepseek-ai/dsh-llm": "^0.1.6-alpha.2",
86
+ "@deepseek-ai/dsh-mcp-client": "^0.1.6-alpha.2",
87
+ "@deepseek-ai/dsh-system-prompt": "^0.1.6-alpha.2",
78
88
  "@deepseek-ai/dsh-tools": "^0.1.6-alpha.2",
79
89
  "@deepseek-ai/schemastery": "^3.18.3"
80
90
  }