@logictan/dsh-browser-agent 0.1.0 → 0.2.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.
package/cordis.patch.yml CHANGED
@@ -1,9 +1,9 @@
1
1
  # dsh-browser-agent bundle patch layer.
2
2
  #
3
3
  # One bare plugin row. The package ships two halves — the host half
4
- # (exports ".") registers the `browser-agent` settings namespace and the
5
- # `browser_agent` model tool, the browser half (exports "./client") registers
6
- # the Plugins-page configuration card.
4
+ # (exports ".") registers the `browser_agent` model tool and exports the
5
+ # `Config` schema the Plugins page derives its form from, the browser half
6
+ # (exports "./client") registers the Plugins-page configuration card.
7
7
  #
8
8
  # The row must stay a BARE package name: the web plugin table locates each
9
9
  # package's `dsh.client` manifest from the specifier of the loader row that
package/lib/client.js CHANGED
@@ -11,13 +11,29 @@
11
11
  *
12
12
  * The field list mirrors the schema in `src/config.js`; the two are kept in
13
13
  * step by hand because this half ships as plain browser code.
14
+ *
15
+ * As of the 0.1.7-alpha.1 settings redesign the settings service is
16
+ * `configForms`, not `settingsScope`: a form is addressed by loader ENTRY ID
17
+ * (`configForms.get(entryId)`) rather than bound by namespace, and the shared
18
+ * describe mirror is reached through `configForms.describe()`. The service is
19
+ * injected NESTED inside `apply` rather than declared in `inject`, so a host
20
+ * without the settings UI loses only this card instead of failing the page's
21
+ * boot audit.
14
22
  */
15
23
  window.__ModuleLoader__.load({
16
24
  id: "@logictan/dsh-browser-agent",
17
25
  factory: function (require) {
18
26
  var React = require("react");
19
27
 
20
- /** Settings namespace the host half registers. */
28
+ /**
29
+ * Loader entry id this card's form is keyed by.
30
+ *
31
+ * `configForms.get(entryId)` addresses the entry the Host serves, and
32
+ * `dsh-settings` keys that entry by profile entry id — which the repo
33
+ * convention pins to the host half's `export const name`. It is the same
34
+ * string the mirror rows carry as `ns`, which is how the secret sidecar
35
+ * below is found.
36
+ */
21
37
  var NS = "browser-agent";
22
38
  /** The patch row this card configures (cordis.patch.yml). */
23
39
  var ROW_ID = "browser-agent";
@@ -256,14 +272,17 @@ window.__ModuleLoader__.load({
256
272
  * whether a key is set — overridden() is false for a saved key, which would
257
273
  * make the reset button skip it and silently leave it stored. The describe
258
274
  * sidecar is the one read that carries the fact: a { path, set } entry per
259
- * schema-declared secret, with the value itself never sent. The bound
260
- * namespace scope does not project the sidecar, so this reads the shared
261
- * describe mirror directly.
275
+ * schema-declared secret, with the value itself never sent.
276
+ *
277
+ * The shared describe mirror is reached through `configForms.describe()`.
278
+ * It exposes the whole document — `view.namespaces`, one row per served
279
+ * entry — and the form controller bound to this entry does NOT project the
280
+ * sidecar, so the lookup is done here against the mirror's own snapshot.
262
281
  *
263
282
  * @returns a map from field key to whether that secret currently has a value.
264
283
  */
265
284
  function useSecretStatus(ctx) {
266
- var mirror = ctx.settingsScope.describe();
285
+ var mirror = ctx.configForms.describe();
267
286
  var subscribe = React.useCallback(
268
287
  function (onChange) {
269
288
  return mirror.subscribe(onChange);
@@ -276,9 +295,13 @@ window.__ModuleLoader__.load({
276
295
  },
277
296
  [mirror],
278
297
  );
279
- React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
298
+ var snapshot = React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
280
299
 
281
- var row = mirror.namespace(NS);
300
+ var namespaces = (snapshot && snapshot.view && snapshot.view.namespaces) || [];
301
+ var row = null;
302
+ for (var i = 0; i < namespaces.length; i++) {
303
+ if (namespaces[i].ns === NS) row = namespaces[i];
304
+ }
282
305
  var set = {};
283
306
  ((row && row.secrets) || []).forEach(function (secret) {
284
307
  if (secret.path.length === 1 && secret.set) set[secret.path[0]] = true;
@@ -312,8 +335,9 @@ window.__ModuleLoader__.load({
312
335
  /**
313
336
  * The row's configuration card.
314
337
  *
315
- * `scope` is the settings namespace bound on this plugin's own fiber by
316
- * `apply`; `ctx` supplies the live model directory.
338
+ * `scope` is the configuration form `apply` obtained for this entry from
339
+ * `configForms.get(NS)`; `ctx` is the scoped context it was obtained on,
340
+ * which supplies the live model directory.
317
341
  */
318
342
  function BrowserAgentSettingsCard(props) {
319
343
  var scope = props.scope;
@@ -793,30 +817,44 @@ window.__ModuleLoader__.load({
793
817
  }
794
818
 
795
819
  /**
796
- * Client plugin body: bind the settings namespace and register the card.
820
+ * Client plugin body: bind the configuration form and register the card.
821
+ *
822
+ * `configForms` is injected NESTED rather than declared in this plugin's
823
+ * `inject` list. A plugin that waits on a service the profile does not
824
+ * provide never activates, and the Web UI's boot audit turns that into a
825
+ * hard failure for the WHOLE page ("web boot: N entries did not activate"),
826
+ * not just for this card. Nested, a host without the settings UI loses only
827
+ * this card — which is the same trade `dsh-market` makes.
828
+ *
829
+ * The nested context inherits the services declared here (`slots`,
830
+ * `remote`), so the card is handed the scoped context and can reach both the
831
+ * form and the model directory through it.
832
+ *
797
833
  * @param ctx - client root context.
798
834
  */
799
835
  function apply(ctx) {
800
- var scope = ctx.settingsScope.bind({ namespace: NS });
801
- BUNDLE_NAMES.forEach(function (bundle) {
802
- ctx.slots.inject("plugins.row.config", function () {
803
- return ctx.slots.register(
804
- {
805
- name: "plugins.row.config",
806
- key: bundle + "#" + ROW_ID,
807
- inject: function () {
808
- return { scope: scope, ctx: ctx };
836
+ ctx.inject(["configForms"], function (scoped) {
837
+ var scope = scoped.configForms.get(NS);
838
+ BUNDLE_NAMES.forEach(function (bundle) {
839
+ scoped.slots.inject("plugins.row.config", function () {
840
+ return scoped.slots.register(
841
+ {
842
+ name: "plugins.row.config",
843
+ key: bundle + "#" + ROW_ID,
844
+ inject: function () {
845
+ return { scope: scope, ctx: scoped };
846
+ },
809
847
  },
810
- },
811
- BrowserAgentSettingsCard,
812
- );
848
+ BrowserAgentSettingsCard,
849
+ );
850
+ });
813
851
  });
814
852
  });
815
853
  }
816
854
 
817
855
  return {
818
856
  name: "dsh-browser-agent",
819
- inject: ["slots", "settingsScope", "remote", "remote.session"],
857
+ inject: ["slots", "remote", "remote.session"],
820
858
  apply: apply,
821
859
  };
822
860
  },
package/lib/config.js CHANGED
@@ -1,11 +1,24 @@
1
1
  /**
2
2
  * Configuration surface of the browser agent.
3
3
  *
4
- * The settings namespace is the one contract three surfaces share: the host
5
- * half reads it when a run starts, the Plugins-page card edits it, and the
6
- * client half's field list mirrors it. The card ships as plain browser code and
7
- * cannot import this module, so the two field lists are kept in step by hand
8
- * changing a key here means changing `src/client.js` too.
4
+ * As of the 0.1.7-alpha.1 settings redesign this schema is the ONE contract
5
+ * every surface shares: the host half reads it when a run starts, and
6
+ * `dsh-settings` derives the Plugins-page form from it keyed by the loader
7
+ * entry id, which is why {@link ENTRY_ID} must equal both the row id in
8
+ * `cordis.patch.yml` and the host half's `export const name`. The card ships as
9
+ * plain browser code and cannot import this module, so its field list is kept in
10
+ * step by hand — changing a key here means changing `src/client.js` too.
11
+ *
12
+ * Two rules follow from the redesign, and both are load-bearing:
13
+ *
14
+ * - The schema must be reachable as `entry.fiber.runtime.Config`, i.e. exported
15
+ * from the PACKAGE ENTRY (`lib/index.js`), not merely from this module. That
16
+ * is where `dsh-settings` looks; a schema exported only from a subpath is
17
+ * invisible and the entry simply has no form.
18
+ * - Every field must be `volatile()`. The marker is what makes the field
19
+ * editable without a remount — and it is also what makes the Loader hand
20
+ * `apply` a live `{ get() }` reference instead of a value, which is the only
21
+ * shape the host half reads.
9
22
  *
10
23
  * @module @logictan/dsh-browser-agent/config
11
24
  */
@@ -14,11 +27,18 @@ import { DEFAULT_ENDPOINT as DEFAULT_CDP_ENDPOINT } from './cdp.js';
14
27
  import { DEFAULT_MAX_STEPS } from './loop.js';
15
28
  import { DEFAULT_ENDPOINT as DEFAULT_TYPESAFE_ENDPOINT, DEFAULT_MODEL } from './typesafe.js';
16
29
 
17
- /** Settings namespace the host half registers and the card binds. */
18
- export const SETTINGS_NAMESPACE = 'browser-agent';
30
+ /**
31
+ * The loader entry id this plugin's configuration form is keyed by.
32
+ *
33
+ * `dsh-settings` keys a form by the profile entry id, which the repo convention
34
+ * pins to the host half's `export const name`; the browser half passes the same
35
+ * string to `configForms.get`. A mismatch does not throw — the card simply binds
36
+ * a form that is never served, and every field renders as unset.
37
+ */
38
+ export const ENTRY_ID = 'browser-agent';
19
39
 
20
40
  /**
21
- * Schema of the `browser-agent` settings section.
41
+ * Schema of the `browser-agent` configuration.
22
42
  *
23
43
  * The TypeSafe key is `role('secret')`: it is stored in the local settings
24
44
  * document and stripped by every redacting surface, including config sync. That
@@ -35,16 +55,20 @@ export const SETTINGS_NAMESPACE = 'browser-agent';
35
55
  * "use the session's own current route", which is the only default that stays
36
56
  * correct after config sync moves the file to a machine with different
37
57
  * providers; a hard-coded model would point at a model the target may not have.
58
+ *
59
+ * `volatile()` order matters for the secret: `role()` must come first, because
60
+ * `volatile()` wraps the schema and a second wrap throws `volatile schema is
61
+ * already wrapped`.
38
62
  */
39
63
  export const Config = z.object({
40
- typesafeApiKey: z.string().role('secret'),
41
- typesafeEndpoint: z.string().default(DEFAULT_TYPESAFE_ENDPOINT),
42
- typesafeModel: z.string().default(DEFAULT_MODEL),
43
- cdpEndpoint: z.string().default(DEFAULT_CDP_ENDPOINT),
44
- maxSteps: z.number().step(1).min(1).default(DEFAULT_MAX_STEPS),
45
- textProvider: z.string().default(''),
46
- textModel: z.string().default(''),
47
- textReasoningEffort: z.string().default(''),
64
+ typesafeApiKey: z.string().role('secret').volatile(),
65
+ typesafeEndpoint: z.string().default(DEFAULT_TYPESAFE_ENDPOINT).volatile(),
66
+ typesafeModel: z.string().default(DEFAULT_MODEL).volatile(),
67
+ cdpEndpoint: z.string().default(DEFAULT_CDP_ENDPOINT).volatile(),
68
+ maxSteps: z.number().step(1).min(1).default(DEFAULT_MAX_STEPS).volatile(),
69
+ textProvider: z.string().default('').volatile(),
70
+ textModel: z.string().default('').volatile(),
71
+ textReasoningEffort: z.string().default('').volatile(),
48
72
  });
49
73
 
50
74
  /**
@@ -55,11 +79,17 @@ export const Config = z.object({
55
79
  * three-step fallback (configured pair, then the session's routed request, then
56
80
  * its options) that `ctx-mem` uses for its fill call.
57
81
  *
58
- * @param config - the resolved settings section.
59
- * @param agent - the calling agent, when the tool was invoked from a turn.
82
+ * The second argument is the tool EXECUTION CONTEXT, not the agent: that is
83
+ * what the tool body has in hand. The two are not interchangeable reading
84
+ * agent fields off the context short-circuits every optional chain and silently
85
+ * reports "unconfigured" on a session that has a perfectly good route.
86
+ *
87
+ * @param config - the configuration in effect, as plain values (the host half
88
+ * unwraps the live references before calling).
89
+ * @param exec - the tool execution context; its `agent` is the caller.
60
90
  * @returns the route to call; empty provider/model means "unconfigured".
61
91
  */
62
- export function resolveTextRoute(config, agent) {
92
+ export function resolveTextRoute(config, exec) {
63
93
  if (config.textProvider !== '' && config.textModel !== '') {
64
94
  return {
65
95
  provider: config.textProvider,
@@ -68,6 +98,7 @@ export function resolveTextRoute(config, agent) {
68
98
  };
69
99
  }
70
100
 
101
+ const agent = exec?.agent;
71
102
  const routed = agent?.session?.requestHeader?.()?.config;
72
103
  if (routed?.provider && routed?.model) {
73
104
  return { provider: routed.provider, model: routed.model, reasoningEffort: '' };
package/lib/index.js CHANGED
@@ -1,15 +1,26 @@
1
1
  /**
2
2
  * dsh-browser-agent — host half.
3
3
  *
4
- * Registers two things: the `browser-agent` settings namespace, and the
5
- * `browser_agent` model tool that runs the decision loop over CDP against the
6
- * user's own Chrome.
4
+ * Registers one thing: the `browser_agent` model tool that runs the decision
5
+ * loop over CDP against the user's own Chrome.
6
+ *
7
+ * Configuration arrives as `apply`'s second argument. As of the 0.1.7-alpha.1
8
+ * settings redesign the plugin's own `Config` schema is the only config surface
9
+ * — `dsh-settings` derives the Plugins-page form from `entry.fiber.runtime.Config`,
10
+ * keyed by the entry id — and every field of it is `volatile()`, so the Loader
11
+ * hands `apply` one live `{ get() }` reference per field rather than a value.
12
+ * The former `settings.register(ns, schema)` and `settings.get(ns)` seams were
13
+ * removed in that generation, which is why this plugin failed to activate on it.
14
+ *
15
+ * `Config` is therefore re-exported from the package entry: that is the exact
16
+ * object `dsh-settings` looks for, and a schema exported only from `./config.js`
17
+ * would leave the entry with no form at all.
7
18
  *
8
19
  * The tool is registered through `ctx.get('tools')` rather than the `ctx.tools`
9
20
  * property. Cordis requires a declared `inject` before a service property may
10
21
  * be read, and `tools` is genuinely optional here — a headless deployment that
11
- * composes no tool registry still has a working settings namespace — so it must
12
- * not be injected. Reading it optionally is the same choice
22
+ * composes no tool registry still has a working configuration — so it must not
23
+ * be injected. Reading it optionally is the same choice
13
24
  * `dsh-config-manager`'s `registerModelTools` makes, for the same reason.
14
25
  *
15
26
  * @module @logictan/dsh-browser-agent
@@ -17,9 +28,11 @@
17
28
  import { defineTool } from '@deepseek-ai/dsh-tools';
18
29
 
19
30
  import { attach, activePage } from './cdp.js';
20
- import { Config, SETTINGS_NAMESPACE, resolveTextRoute } from './config.js';
31
+ import { resolveTextRoute } from './config.js';
21
32
  import { run } from './loop.js';
22
33
 
34
+ export { Config } from './config.js';
35
+
23
36
  /** Plugin row id; must equal the row id in `cordis.patch.yml`. */
24
37
  export const name = 'browser-agent';
25
38
 
@@ -27,25 +40,47 @@ export const name = 'browser-agent';
27
40
  * Services this plugin needs before it can mount.
28
41
  *
29
42
  * `llm` is required: the TYPE_TEXT step cannot work without it, and a silent
30
- * degradation there would be worse than a load failure. `settings` is required
31
- * for the same reason every run reads its configuration from it.
43
+ * degradation there would be worse than a load failure. `settings` is NOT
44
+ * injected the plugin neither reads nor registers a settings namespace any
45
+ * more, and waiting on a service a profile does not provide is precisely what
46
+ * left this entry `pending` (and took the whole Web UI's boot audit down) on
47
+ * 0.1.7-alpha.1.
32
48
  */
33
- export const inject = ['settings', 'llm'];
49
+ export const inject = ['llm'];
34
50
 
35
51
  /**
36
52
  * Register the `browser_agent` tool.
37
53
  *
38
54
  * @param ctx - the plugin context.
55
+ * @param config - one live reference per {@link Config} field.
39
56
  */
40
- export function apply(ctx) {
41
- const settings = ctx.settings;
57
+ export function apply(ctx, config) {
42
58
  const llm = ctx.llm;
43
59
  const tools = ctx.get('tools');
44
60
 
45
- // The namespace must be registered before anything reads or writes it: an
46
- // unregistered namespace makes `settings.get` return undefined and every
47
- // Plugins-page write fail with "settings namespace ... is not registered".
48
- settings.register(SETTINGS_NAMESPACE, Config);
61
+ /**
62
+ * The configuration in effect right now, as plain values.
63
+ *
64
+ * Resolved at each use rather than captured at activation: the Loader commits
65
+ * a Plugins-page edit into these very references (`loader/volatile-update`)
66
+ * instead of remounting the plugin, so a snapshot taken here would pin the
67
+ * values the profile started with and the card's save button would appear to
68
+ * do nothing.
69
+ *
70
+ * @returns the resolved configuration.
71
+ */
72
+ function currentConfig() {
73
+ return {
74
+ typesafeApiKey: config.typesafeApiKey.get(),
75
+ typesafeEndpoint: config.typesafeEndpoint.get(),
76
+ typesafeModel: config.typesafeModel.get(),
77
+ cdpEndpoint: config.cdpEndpoint.get(),
78
+ maxSteps: config.maxSteps.get(),
79
+ textProvider: config.textProvider.get(),
80
+ textModel: config.textModel.get(),
81
+ textReasoningEffort: config.textReasoningEffort.get(),
82
+ };
83
+ }
49
84
 
50
85
  const definition = defineTool({
51
86
  name: 'browser_agent',
@@ -62,7 +97,7 @@ export function apply(ctx) {
62
97
  render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
63
98
  },
64
99
  async execute(args, exec) {
65
- const current = Config(settings.get(SETTINGS_NAMESPACE) ?? {});
100
+ const current = currentConfig();
66
101
  // An unset optional field resolves to undefined; a cleared one to ''.
67
102
  if (current.typesafeApiKey === undefined || current.typesafeApiKey === '') {
68
103
  throw new Error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logictan/dsh-browser-agent",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "DSH browser agent: a dynamic operation+target decision loop. TypeSafe picks the action, this plugin executes it over CDP against the user's own Chrome. DSH 浏览器 Agent 子插件。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -16,7 +16,6 @@
16
16
  "client": {
17
17
  "inject": [
18
18
  "slots",
19
- "settingsScope",
20
19
  "remote",
21
20
  "remote.session"
22
21
  ],
@@ -59,9 +58,8 @@
59
58
  "peerDependencies": {
60
59
  "@deepseek-ai/cordis": "^4.0.2",
61
60
  "@deepseek-ai/dsh-llm": "^0.1.6-alpha.2",
62
- "@deepseek-ai/dsh-settings": "^0.1.6-alpha.2",
63
61
  "@deepseek-ai/dsh-tools": "^0.1.6-alpha.2",
64
- "@deepseek-ai/schemastery": "^3.18.2"
62
+ "@deepseek-ai/schemastery": "^3.18.3"
65
63
  },
66
64
  "peerDependenciesMeta": {
67
65
  "@deepseek-ai/cordis": {
@@ -70,9 +68,6 @@
70
68
  "@deepseek-ai/dsh-llm": {
71
69
  "optional": true
72
70
  },
73
- "@deepseek-ai/dsh-settings": {
74
- "optional": true
75
- },
76
71
  "@deepseek-ai/dsh-tools": {
77
72
  "optional": true
78
73
  },
@@ -83,8 +78,7 @@
83
78
  "devDependencies": {
84
79
  "@deepseek-ai/cordis": "^4.0.2",
85
80
  "@deepseek-ai/dsh-llm": "^0.1.6-alpha.2",
86
- "@deepseek-ai/dsh-settings": "^0.1.6-alpha.2",
87
81
  "@deepseek-ai/dsh-tools": "^0.1.6-alpha.2",
88
- "@deepseek-ai/schemastery": "^3.18.2"
82
+ "@deepseek-ai/schemastery": "^3.18.3"
89
83
  }
90
84
  }