@huaqiu/dsh-eda-host 0.3.22 → 0.3.25

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/src/tools.ts CHANGED
@@ -1,24 +1,30 @@
1
1
  /**
2
2
  * Agent tools for `@huaqiu/dsh-eda-host`.
3
3
  *
4
- * Three semantic operations, one per netlist scope:
4
+ * Three semantic netlist operations, one per scope:
5
5
  *
6
6
  * get_project_netlist complete logical netlist of the current project
7
7
  * get_selection_netlist netlist of the currently selected components
8
8
  * get_active_page_netlist netlist of the active schematic page
9
9
  *
10
- * The tools are pure pass-throughs: they call the hq-edge netlist router and
11
- * return the semantic `SchematicNetlist` as lossless JSON. Errors are
12
- * propagated with a semantic `kind` (FAILED_PRECONDITION / UNIMPLEMENTED /
13
- * INTERNAL / UNAVAILABLE) never converted into a fake empty netlist. A
14
- * valid-but-empty netlist is `ok: true` with empty `components`/`nets`.
10
+ * Two EDA host discovery operations:
11
+ *
12
+ * get_eda_host_info which host, which version, where it is installed
13
+ * get_eda_host_capabilities what the current host can actually do
14
+ *
15
+ * The netlist tools are pure pass-throughs: they call the hq-edge netlist
16
+ * router and return the semantic `SchematicNetlist` as lossless JSON. Errors
17
+ * are propagated with a semantic `kind` (FAILED_PRECONDITION / UNIMPLEMENTED /
18
+ * INTERNAL / UNAVAILABLE / DEADLINE_EXCEEDED) — never converted into a fake
19
+ * empty netlist. A valid-but-empty netlist is `ok: true` with empty
20
+ * `components`/`nets`.
15
21
  *
16
22
  * @module
17
23
  */
18
24
 
19
25
  import { defineTool } from '@deepseek-ai/dsh-tools'
20
- import type { EdaHostClient } from './client.js'
21
- import { NetlistError, type SchematicNetlist } from './types.js'
26
+ import type { EdaHostClient, EdaHostRequestOptions } from './client.js'
27
+ import { NetlistError, type EdaHostCapability, type EdaHostInfo, type SchematicNetlist } from './types.js'
22
28
 
23
29
  /** Structural alias of the DSH `JsonValue`. */
24
30
  type Json = string | number | boolean | null | Json[] | { [key: string]: Json }
@@ -48,24 +54,47 @@ type ScopeResult =
48
54
  | { ok: true; scope: NetlistScopeKind; netlist: SchematicNetlist }
49
55
  | { ok: false; scope: NetlistScopeKind; error: { kind: string; message: string } }
50
56
 
57
+ type HostInfoResult =
58
+ | { ok: true; info: EdaHostInfo }
59
+ | { ok: false; error: { kind: string; message: string } }
60
+
61
+ type HostCapabilitiesResult =
62
+ | { ok: true; capabilities: EdaHostCapability[] }
63
+ | { ok: false; error: { kind: string; message: string } }
64
+
65
+ /**
66
+ * Every failure kind, and what the agent should do about it.
67
+ *
68
+ * Shared by all tools so the prompt contract cannot drift between them.
69
+ */
70
+ const ERROR_SEMANTICS =
71
+ `IMPORTANT SEMANTICS: on ok:false, error.kind distinguishes the cause: ` +
72
+ `"FAILED_PRECONDITION" (no EDA host / no live editor — ask the user to open the design in ` +
73
+ `the EDA editor first, then retry), "UNIMPLEMENTED" (this capability is not supported by ` +
74
+ `the current host — do NOT retry; report it to the user), "UNAVAILABLE" (hq-edge / EDA host ` +
75
+ `unreachable), "DEADLINE_EXCEEDED" (the host did not answer in time — retry once, then ` +
76
+ `report), "INTERNAL" (host-side failure). Do NOT fabricate data.`
77
+
78
+ function failureOf(err: unknown): { kind: string; message: string } {
79
+ const kind = err instanceof NetlistError ? err.kind : ('INTERNAL' as const)
80
+ return { kind, message: String((err as Error)?.message ?? err) }
81
+ }
82
+
51
83
  async function runScope(
52
84
  env: NetlistToolEnv,
53
85
  scope: NetlistScopeKind,
86
+ exec?: ToolExecLike,
54
87
  ): Promise<ScopeResult> {
88
+ const options: EdaHostRequestOptions = exec?.signal ? { signal: exec.signal } : {}
55
89
  try {
56
90
  let netlist: SchematicNetlist
57
- if (scope === 'project') netlist = await env.client.getProjectNetlist()
58
- else if (scope === 'selection') netlist = await env.client.getSelectionNetlist()
59
- else netlist = await env.client.getActivePageNetlist()
91
+ if (scope === 'project') netlist = await env.client.getProjectNetlist(options)
92
+ else if (scope === 'selection') netlist = await env.client.getSelectionNetlist(options)
93
+ else netlist = await env.client.getActivePageNetlist(options)
60
94
 
61
95
  return { ok: true, scope, netlist }
62
96
  } catch (err) {
63
- const kind =
64
- err instanceof NetlistError
65
- ? err.kind
66
- : ('INTERNAL' as const)
67
- const message = String((err as Error)?.message ?? err)
68
- return { ok: false, scope, error: { kind, message } }
97
+ return { ok: false, scope, error: failureOf(err) }
69
98
  }
70
99
  }
71
100
 
@@ -78,12 +107,9 @@ function scopeDescription(scope: NetlistScopeKind, extra: string): string {
78
107
  `Each component has referenceDesignators[], value, manufacturerPartNumber, footprint, ` +
79
108
  `description and pins[] (pinNumber, pinName, electricalType); each net has name and ` +
80
109
  `pinReferences[] (referenceDesignator, pinNumber). ` +
81
- `IMPORTANT SEMANTICS: ok:true with empty components/nets is a VALID empty design — do not ` +
82
- `treat it as a failure. On ok:false, error.kind distinguishes the cause: ` +
83
- `"FAILED_PRECONDITION" (no EDA host / no live editor — ask the user to open the design in ` +
84
- `the EDA editor first, then retry), "UNIMPLEMENTED" (this scope is not supported by the ` +
85
- `current host — do NOT retry; report it to the user), "UNAVAILABLE" (hq-edge host unreachable), ` +
86
- `"INTERNAL" (host-side failure). Do NOT fabricate netlist data.`
110
+ `IMPORTANT: ok:true with empty components/nets is a VALID empty design — do not ` +
111
+ `treat it as a failure. ` +
112
+ ERROR_SEMANTICS
87
113
  )
88
114
  }
89
115
 
@@ -94,8 +120,8 @@ export function createNetListTools(env: NetlistToolEnv) {
94
120
  description: desc,
95
121
  parameters: {},
96
122
  output: { schema: { type: 'json' }, render: renderJson },
97
- async execute(_args: unknown, _exec: ToolExecLike) {
98
- return asJson(await runScope(env, scope))
123
+ async execute(_args: unknown, exec: ToolExecLike) {
124
+ return asJson(await runScope(env, scope, exec))
99
125
  },
100
126
  })
101
127
 
@@ -123,9 +149,74 @@ export function createNetListTools(env: NetlistToolEnv) {
123
149
  'get_active_page_netlist',
124
150
  scopeDescription(
125
151
  'active_page',
126
- 'Returns the netlist for the currently active schematic page. NOTE: KiCad host does ' +
127
- 'not implement this scope — expect ok:false with error.kind "UNIMPLEMENTED".',
152
+ 'Returns the netlist for the currently active schematic page. NOTE: the KiCad host ' +
153
+ 'does not implement this scope — expect ok:false with error.kind "UNIMPLEMENTED". ' +
154
+ 'Check get_eda_host_capabilities before relying on it.',
128
155
  ),
129
156
  ),
130
157
  ]
131
158
  }
159
+
160
+ /**
161
+ * EDA host discovery tools.
162
+ *
163
+ * These expose what the EDA host ALREADY knows and can ALREADY do — they
164
+ * implement no EDA functionality themselves. A capability being advertised is
165
+ * a claim that the host can provide it, nothing more.
166
+ */
167
+ export function createEdaHostTools(env: NetlistToolEnv) {
168
+ return [
169
+ defineTool({
170
+ name: 'get_eda_host_info',
171
+ description:
172
+ `Describe the EDA host currently connected through hq-edge (DSH → dsh-eda-host → ` +
173
+ `hq-edge → EDA host). Returns { ok, info: { identity: { hostType, hostName, version }, ` +
174
+ `installation: { applicationPath, executables[]: { name, path } } } }. ` +
175
+ `Use it when you need factual information about the current EDA environment, such as ` +
176
+ `"what EDA host am I connected to", "what version is it", "where is it installed", ` +
177
+ `or "where is kicad-cli". ` +
178
+ `The returned information is authoritative host-provided ground truth. ` +
179
+ ERROR_SEMANTICS,
180
+ parameters: {},
181
+ output: { schema: { type: 'json' }, render: renderJson },
182
+ async execute(_args: unknown, exec: ToolExecLike): Promise<Json> {
183
+ try {
184
+ const options: EdaHostRequestOptions = exec?.signal
185
+ ? { signal: exec.signal }
186
+ : {}
187
+ const info = await env.client.getEdaHostInfo(options)
188
+ return asJson<HostInfoResult>({ ok: true, info })
189
+ } catch (err) {
190
+ return asJson<HostInfoResult>({ ok: false, error: failureOf(err) })
191
+ }
192
+ },
193
+ }),
194
+
195
+ defineTool({
196
+ name: 'get_eda_host_capabilities',
197
+ description:
198
+ `List the capabilities the CURRENT EDA host provides, using EDA-independent capability ` +
199
+ `identifiers such as EDA_HOST_CAPABILITY_NETLIST, EDA_HOST_CAPABILITY_PCB or ` +
200
+ `EDA_HOST_CAPABILITY_BOM. Returns { ok, capabilities: string[] }. ` +
201
+ `This is DISCOVERY, not execution: it reports capabilities already provided by the ` +
202
+ `connected EDA host. Treat the returned capability list as the authoritative runtime ` +
203
+ `contract for the current host. Do not assume a capability is available merely because ` +
204
+ `the EDA application is generally known to support it. Unknown capability identifiers ` +
205
+ `MUST be treated as unsupported. ` +
206
+ ERROR_SEMANTICS,
207
+ parameters: {},
208
+ output: { schema: { type: 'json' }, render: renderJson },
209
+ async execute(_args: unknown, exec: ToolExecLike): Promise<Json> {
210
+ try {
211
+ const options: EdaHostRequestOptions = exec?.signal
212
+ ? { signal: exec.signal }
213
+ : {}
214
+ const capabilities = await env.client.getEdaHostCapabilities(options)
215
+ return asJson<HostCapabilitiesResult>({ ok: true, capabilities })
216
+ } catch (err) {
217
+ return asJson<HostCapabilitiesResult>({ ok: false, error: failureOf(err) })
218
+ }
219
+ },
220
+ }),
221
+ ]
222
+ }
package/src/types.ts CHANGED
@@ -68,6 +68,8 @@ export type NetlistErrorKind =
68
68
  | 'INTERNAL'
69
69
  /** Host unavailable (connection refused). */
70
70
  | 'UNAVAILABLE'
71
+ /** The host did not answer within the request budget. */
72
+ | 'DEADLINE_EXCEEDED'
71
73
 
72
74
  export class NetlistError extends Error {
73
75
  readonly kind: NetlistErrorKind
@@ -78,3 +80,93 @@ export class NetlistError extends Error {
78
80
  this.kind = kind
79
81
  }
80
82
  }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // EDA host discovery — structural mirror of `hq.host.v1`
86
+ // ---------------------------------------------------------------------------
87
+
88
+ /**
89
+ * `hq.host.v1` carries **no availability flags**.
90
+ *
91
+ * A host that answers `GetEdaHostInfo` is, by definition, available, and every
92
+ * executable it lists is one it can actually run. Unavailability is therefore
93
+ * never a field to check — it is a failed request, surfaced as `ok:false` with
94
+ * `error.kind` `UNAVAILABLE` / `FAILED_PRECONDITION` / `DEADLINE_EXCEEDED`.
95
+ *
96
+ * Consequence for consumers: never infer "unavailable" from a missing or empty
97
+ * value. Absence of `identity`/`installation` just means proto3 omitted
98
+ * defaults (see `parseEdaHostInfo`), and empty `path` only means the host could
99
+ * not resolve an absolute location — the tool is still runnable by name.
100
+ */
101
+
102
+ /**
103
+ * Which EDA application sits behind the semantic host boundary.
104
+ *
105
+ * Deliberately EDA-independent: a new host adds a value here rather than
106
+ * introducing host-specific messages or tools.
107
+ */
108
+ export type EdaHostType =
109
+ | 'EDA_HOST_TYPE_UNSPECIFIED'
110
+ | 'EDA_HOST_TYPE_KICAD'
111
+ | 'EDA_HOST_TYPE_HQ_EDA'
112
+
113
+ /**
114
+ * A capability an EDA host may provide.
115
+ *
116
+ * A capability is advertised only when the host can actually provide it —
117
+ * discovering a capability is not the same as implementing it. Unknown values
118
+ * MUST be treated as "not supported" so a newer host cannot confuse an older
119
+ * plugin.
120
+ */
121
+ export type EdaHostCapability =
122
+ | 'EDA_HOST_CAPABILITY_UNSPECIFIED'
123
+ | 'EDA_HOST_CAPABILITY_SCHEMATIC'
124
+ | 'EDA_HOST_CAPABILITY_PCB'
125
+ | 'EDA_HOST_CAPABILITY_NETLIST'
126
+ | 'EDA_HOST_CAPABILITY_NETLIST_SELECTION'
127
+ | 'EDA_HOST_CAPABILITY_NETLIST_ACTIVE_PAGE'
128
+ | 'EDA_HOST_CAPABILITY_ERC'
129
+ | 'EDA_HOST_CAPABILITY_DRC'
130
+ | 'EDA_HOST_CAPABILITY_BOM'
131
+ | 'EDA_HOST_CAPABILITY_PLACEMENT'
132
+
133
+ /** A host-provided command line tool. */
134
+ export interface EdaHostExecutable {
135
+ /** Stable tool name, e.g. "kicad-cli". */
136
+ name: string
137
+ /**
138
+ * Absolute path when the host could resolve one.
139
+ *
140
+ * Empty means "resolvable by name only" (e.g. found on `PATH` but the host
141
+ * did not report a location) — never "not installed" or "unusable".
142
+ */
143
+ path: string
144
+ }
145
+
146
+ /** Where the host application lives on disk. */
147
+ export interface EdaHostInstallation {
148
+ applicationPath: string
149
+ /**
150
+ * Executables the host can run. Being listed here IS the availability
151
+ * signal: there is no per-executable availability flag.
152
+ */
153
+ executables: EdaHostExecutable[]
154
+ }
155
+
156
+ /** Which EDA host is connected. */
157
+ export interface EdaHostIdentity {
158
+ hostType: EdaHostType
159
+ hostName: string
160
+ version: string
161
+ }
162
+
163
+ /**
164
+ * EDA-independent description of the connected host.
165
+ *
166
+ * Answering this message is the host's way of saying it is available — there is
167
+ * no `available` field to inspect.
168
+ */
169
+ export interface EdaHostInfo {
170
+ identity: EdaHostIdentity
171
+ installation: EdaHostInstallation
172
+ }