@huaqiu/dsh-eda-host 0.3.24 → 0.4.1

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 PcbSelection, 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,51 @@ 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
+ type PcbSelectionResult =
66
+ | { ok: true; selection: PcbSelection }
67
+ | { ok: false; error: { kind: string; message: string } }
68
+
69
+ /**
70
+ * Every failure kind, and what the agent should do about it.
71
+ *
72
+ * Shared by all tools so the prompt contract cannot drift between them.
73
+ */
74
+ const ERROR_SEMANTICS =
75
+ `IMPORTANT SEMANTICS: on ok:false, error.kind distinguishes the cause: ` +
76
+ `"FAILED_PRECONDITION" (no EDA host / no live editor — ask the user to open the design in ` +
77
+ `the EDA editor first, then retry), "UNIMPLEMENTED" (this capability is not supported by ` +
78
+ `the current host — do NOT retry; report it to the user), "UNAVAILABLE" (hq-edge / EDA host ` +
79
+ `unreachable), "DEADLINE_EXCEEDED" (the host did not answer in time — retry once, then ` +
80
+ `report), "INTERNAL" (host-side failure). Do NOT fabricate data.`
81
+
82
+ function failureOf(err: unknown): { kind: string; message: string } {
83
+ const kind = err instanceof NetlistError ? err.kind : ('INTERNAL' as const)
84
+ return { kind, message: String((err as Error)?.message ?? err) }
85
+ }
86
+
51
87
  async function runScope(
52
88
  env: NetlistToolEnv,
53
89
  scope: NetlistScopeKind,
90
+ exec?: ToolExecLike,
54
91
  ): Promise<ScopeResult> {
92
+ const options: EdaHostRequestOptions = exec?.signal ? { signal: exec.signal } : {}
55
93
  try {
56
94
  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()
95
+ if (scope === 'project') netlist = await env.client.getProjectNetlist(options)
96
+ else if (scope === 'selection') netlist = await env.client.getSelectionNetlist(options)
97
+ else netlist = await env.client.getActivePageNetlist(options)
60
98
 
61
99
  return { ok: true, scope, netlist }
62
100
  } 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 } }
101
+ return { ok: false, scope, error: failureOf(err) }
69
102
  }
70
103
  }
71
104
 
@@ -78,12 +111,9 @@ function scopeDescription(scope: NetlistScopeKind, extra: string): string {
78
111
  `Each component has referenceDesignators[], value, manufacturerPartNumber, footprint, ` +
79
112
  `description and pins[] (pinNumber, pinName, electricalType); each net has name and ` +
80
113
  `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.`
114
+ `IMPORTANT: ok:true with empty components/nets is a VALID empty design — do not ` +
115
+ `treat it as a failure. ` +
116
+ ERROR_SEMANTICS
87
117
  )
88
118
  }
89
119
 
@@ -94,8 +124,8 @@ export function createNetListTools(env: NetlistToolEnv) {
94
124
  description: desc,
95
125
  parameters: {},
96
126
  output: { schema: { type: 'json' }, render: renderJson },
97
- async execute(_args: unknown, _exec: ToolExecLike) {
98
- return asJson(await runScope(env, scope))
127
+ async execute(_args: unknown, exec: ToolExecLike) {
128
+ return asJson(await runScope(env, scope, exec))
99
129
  },
100
130
  })
101
131
 
@@ -123,9 +153,104 @@ export function createNetListTools(env: NetlistToolEnv) {
123
153
  'get_active_page_netlist',
124
154
  scopeDescription(
125
155
  '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".',
156
+ 'Returns the netlist for the currently active schematic page. NOTE: the KiCad host ' +
157
+ 'does not implement this scope — expect ok:false with error.kind "UNIMPLEMENTED". ' +
158
+ 'Check get_eda_host_capabilities before relying on it.',
128
159
  ),
129
160
  ),
130
161
  ]
131
162
  }
163
+
164
+ /**
165
+ * EDA host discovery tools.
166
+ *
167
+ * These expose what the EDA host ALREADY knows and can ALREADY do — they
168
+ * implement no EDA functionality themselves. A capability being advertised is
169
+ * a claim that the host can provide it, nothing more.
170
+ */
171
+ export function createEdaHostTools(env: NetlistToolEnv) {
172
+ return [
173
+ defineTool({
174
+ name: 'get_eda_host_info',
175
+ description:
176
+ `Describe the EDA host currently connected through hq-edge (DSH → dsh-eda-host → ` +
177
+ `hq-edge → EDA host). Returns { ok, info: { identity: { hostType, hostName, version }, ` +
178
+ `installation: { applicationPath, executables[]: { name, path } } } }. ` +
179
+ `Use it when you need factual information about the current EDA environment, such as ` +
180
+ `"what EDA host am I connected to", "what version is it", "where is it installed", ` +
181
+ `or "where is kicad-cli". ` +
182
+ `The returned information is authoritative host-provided ground truth. ` +
183
+ ERROR_SEMANTICS,
184
+ parameters: {},
185
+ output: { schema: { type: 'json' }, render: renderJson },
186
+ async execute(_args: unknown, exec: ToolExecLike): Promise<Json> {
187
+ try {
188
+ const options: EdaHostRequestOptions = exec?.signal
189
+ ? { signal: exec.signal }
190
+ : {}
191
+ const info = await env.client.getEdaHostInfo(options)
192
+ return asJson<HostInfoResult>({ ok: true, info })
193
+ } catch (err) {
194
+ return asJson<HostInfoResult>({ ok: false, error: failureOf(err) })
195
+ }
196
+ },
197
+ }),
198
+
199
+ defineTool({
200
+ name: 'get_pcb_selection',
201
+ description:
202
+ `Read the semantic PCB selection from the current PCB editor through hq-edge ` +
203
+ `(DSH → dsh-eda-host → hq-edge → EDA host). Returns { ok, selection: { footprints[], ` +
204
+ `pads[], tracks[], arcs[], vias[], zones[], shapes[], texts[], dimensions[], groups[], ` +
205
+ `nets[] } }. ` +
206
+ `Each footprint has reference, value, footprint, position {x,y} in mm, rotationDeg and ` +
207
+ `pads[] (pin, type, shape, position, widthMm, heightMm, rotationDeg, layer, net{name, ` +
208
+ `code}); tracks have layer, start/end in mm, widthMm, lengthMm and net; vias have ` +
209
+ `layers[], drillMm, viaType and start/end; zones have layer, net and outline segments. ` +
210
+ `Every object carries id — the EDA-host native object identity. ` +
211
+ `IMPORTANT: ok:true with all-empty arrays is a VALID empty selection (nothing selected) ` +
212
+ `— do not treat it as a failure. ` +
213
+ ERROR_SEMANTICS,
214
+ parameters: {},
215
+ output: { schema: { type: 'json' }, render: renderJson },
216
+ async execute(_args: unknown, exec: ToolExecLike): Promise<Json> {
217
+ try {
218
+ const options: EdaHostRequestOptions = exec?.signal
219
+ ? { signal: exec.signal }
220
+ : {}
221
+ const selection = await env.client.getPcbSelection(options)
222
+ return asJson<PcbSelectionResult>({ ok: true, selection })
223
+ } catch (err) {
224
+ return asJson<PcbSelectionResult>({ ok: false, error: failureOf(err) })
225
+ }
226
+ },
227
+ }),
228
+
229
+ defineTool({
230
+ name: 'get_eda_host_capabilities',
231
+ description:
232
+ `List the capabilities the CURRENT EDA host provides, using EDA-independent capability ` +
233
+ `identifiers such as EDA_HOST_CAPABILITY_NETLIST, EDA_HOST_CAPABILITY_PCB or ` +
234
+ `EDA_HOST_CAPABILITY_BOM. Returns { ok, capabilities: string[] }. ` +
235
+ `This is DISCOVERY, not execution: it reports capabilities already provided by the ` +
236
+ `connected EDA host. Treat the returned capability list as the authoritative runtime ` +
237
+ `contract for the current host. Do not assume a capability is available merely because ` +
238
+ `the EDA application is generally known to support it. Unknown capability identifiers ` +
239
+ `MUST be treated as unsupported. ` +
240
+ ERROR_SEMANTICS,
241
+ parameters: {},
242
+ output: { schema: { type: 'json' }, render: renderJson },
243
+ async execute(_args: unknown, exec: ToolExecLike): Promise<Json> {
244
+ try {
245
+ const options: EdaHostRequestOptions = exec?.signal
246
+ ? { signal: exec.signal }
247
+ : {}
248
+ const capabilities = await env.client.getEdaHostCapabilities(options)
249
+ return asJson<HostCapabilitiesResult>({ ok: true, capabilities })
250
+ } catch (err) {
251
+ return asJson<HostCapabilitiesResult>({ ok: false, error: failureOf(err) })
252
+ }
253
+ },
254
+ }),
255
+ ]
256
+ }
package/src/types.ts CHANGED
@@ -54,6 +54,138 @@ export interface SchematicNetlist {
54
54
  nets: ElectricalNet[]
55
55
  }
56
56
 
57
+ /**
58
+ * Semantic PCB selection types.
59
+ *
60
+ * Self-contained structural mirror of `hq.pcb.v1` (the hq-edge-owned semantic
61
+ * protobuf contract for `PcbSelectionService.GetSelection`). Units follow the
62
+ * hq.pcb.v1 convention: positions/sizes/lengths in mm, rotations in degrees,
63
+ * `id` is the KiCad native object identity.
64
+ */
65
+
66
+ export interface PcbPoint {
67
+ x: number
68
+ y: number
69
+ }
70
+
71
+ export interface PcbNetRef {
72
+ name: string
73
+ code: number
74
+ }
75
+
76
+ export interface PcbPad {
77
+ id: string
78
+ pin: string
79
+ type: string
80
+ shape: string
81
+ position?: PcbPoint
82
+ widthMm: number
83
+ heightMm: number
84
+ rotationDeg: number
85
+ layer: string
86
+ net?: PcbNetRef
87
+ }
88
+
89
+ export interface PcbFootprint {
90
+ id: string
91
+ reference: string
92
+ value: string
93
+ footprint: string
94
+ position?: PcbPoint
95
+ rotationDeg: number
96
+ pads: PcbPad[]
97
+ }
98
+
99
+ export interface PcbTrack {
100
+ id: string
101
+ layer: string
102
+ start?: PcbPoint
103
+ end?: PcbPoint
104
+ widthMm: number
105
+ lengthMm: number
106
+ net?: PcbNetRef
107
+ }
108
+
109
+ export interface PcbArc {
110
+ id: string
111
+ layer: string
112
+ start?: PcbPoint
113
+ end?: PcbPoint
114
+ mid?: PcbPoint
115
+ net?: PcbNetRef
116
+ }
117
+
118
+ export interface PcbVia {
119
+ id: string
120
+ layers: string[]
121
+ drillMm: number
122
+ viaType: string
123
+ start?: PcbPoint
124
+ end?: PcbPoint
125
+ }
126
+
127
+ export interface PcbSegment {
128
+ start?: PcbPoint
129
+ end?: PcbPoint
130
+ }
131
+
132
+ export interface PcbZone {
133
+ id: string
134
+ layer: string
135
+ net?: PcbNetRef
136
+ outline: PcbSegment[]
137
+ }
138
+
139
+ export interface PcbShape {
140
+ id: string
141
+ layer: string
142
+ shapeType: string
143
+ start?: PcbPoint
144
+ end?: PcbPoint
145
+ center?: PcbPoint
146
+ radiusMm: number
147
+ mid?: PcbPoint
148
+ widthMm: number
149
+ }
150
+
151
+ export interface PcbText {
152
+ id: string
153
+ layer: string
154
+ text: string
155
+ position?: PcbPoint
156
+ rotationDeg: number
157
+ hJustify: string
158
+ vJustify: string
159
+ }
160
+
161
+ export interface PcbDimension {
162
+ id: string
163
+ layer: string
164
+ start?: PcbPoint
165
+ end?: PcbPoint
166
+ value: string
167
+ dimType: string
168
+ }
169
+
170
+ export interface PcbGroup {
171
+ id: string
172
+ itemIds: string[]
173
+ }
174
+
175
+ export interface PcbSelection {
176
+ footprints: PcbFootprint[]
177
+ pads: PcbPad[]
178
+ tracks: PcbTrack[]
179
+ arcs: PcbArc[]
180
+ vias: PcbVia[]
181
+ zones: PcbZone[]
182
+ shapes: PcbShape[]
183
+ texts: PcbText[]
184
+ dimensions: PcbDimension[]
185
+ groups: PcbGroup[]
186
+ nets: PcbNetRef[]
187
+ }
188
+
57
189
  /**
58
190
  * Semantic error categories for netlist retrieval. Mirrors the gRPC status
59
191
  * contract of `hq.ir.schematic.v1.NetListService` so an agent can distinguish
@@ -68,6 +200,8 @@ export type NetlistErrorKind =
68
200
  | 'INTERNAL'
69
201
  /** Host unavailable (connection refused). */
70
202
  | 'UNAVAILABLE'
203
+ /** The host did not answer within the request budget. */
204
+ | 'DEADLINE_EXCEEDED'
71
205
 
72
206
  export class NetlistError extends Error {
73
207
  readonly kind: NetlistErrorKind
@@ -78,3 +212,93 @@ export class NetlistError extends Error {
78
212
  this.kind = kind
79
213
  }
80
214
  }
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // EDA host discovery — structural mirror of `hq.host.v1`
218
+ // ---------------------------------------------------------------------------
219
+
220
+ /**
221
+ * `hq.host.v1` carries **no availability flags**.
222
+ *
223
+ * A host that answers `GetEdaHostInfo` is, by definition, available, and every
224
+ * executable it lists is one it can actually run. Unavailability is therefore
225
+ * never a field to check — it is a failed request, surfaced as `ok:false` with
226
+ * `error.kind` `UNAVAILABLE` / `FAILED_PRECONDITION` / `DEADLINE_EXCEEDED`.
227
+ *
228
+ * Consequence for consumers: never infer "unavailable" from a missing or empty
229
+ * value. Absence of `identity`/`installation` just means proto3 omitted
230
+ * defaults (see `parseEdaHostInfo`), and empty `path` only means the host could
231
+ * not resolve an absolute location — the tool is still runnable by name.
232
+ */
233
+
234
+ /**
235
+ * Which EDA application sits behind the semantic host boundary.
236
+ *
237
+ * Deliberately EDA-independent: a new host adds a value here rather than
238
+ * introducing host-specific messages or tools.
239
+ */
240
+ export type EdaHostType =
241
+ | 'EDA_HOST_TYPE_UNSPECIFIED'
242
+ | 'EDA_HOST_TYPE_KICAD'
243
+ | 'EDA_HOST_TYPE_HQ_EDA'
244
+
245
+ /**
246
+ * A capability an EDA host may provide.
247
+ *
248
+ * A capability is advertised only when the host can actually provide it —
249
+ * discovering a capability is not the same as implementing it. Unknown values
250
+ * MUST be treated as "not supported" so a newer host cannot confuse an older
251
+ * plugin.
252
+ */
253
+ export type EdaHostCapability =
254
+ | 'EDA_HOST_CAPABILITY_UNSPECIFIED'
255
+ | 'EDA_HOST_CAPABILITY_SCHEMATIC'
256
+ | 'EDA_HOST_CAPABILITY_PCB'
257
+ | 'EDA_HOST_CAPABILITY_NETLIST'
258
+ | 'EDA_HOST_CAPABILITY_NETLIST_SELECTION'
259
+ | 'EDA_HOST_CAPABILITY_NETLIST_ACTIVE_PAGE'
260
+ | 'EDA_HOST_CAPABILITY_ERC'
261
+ | 'EDA_HOST_CAPABILITY_DRC'
262
+ | 'EDA_HOST_CAPABILITY_BOM'
263
+ | 'EDA_HOST_CAPABILITY_PLACEMENT'
264
+
265
+ /** A host-provided command line tool. */
266
+ export interface EdaHostExecutable {
267
+ /** Stable tool name, e.g. "kicad-cli". */
268
+ name: string
269
+ /**
270
+ * Absolute path when the host could resolve one.
271
+ *
272
+ * Empty means "resolvable by name only" (e.g. found on `PATH` but the host
273
+ * did not report a location) — never "not installed" or "unusable".
274
+ */
275
+ path: string
276
+ }
277
+
278
+ /** Where the host application lives on disk. */
279
+ export interface EdaHostInstallation {
280
+ applicationPath: string
281
+ /**
282
+ * Executables the host can run. Being listed here IS the availability
283
+ * signal: there is no per-executable availability flag.
284
+ */
285
+ executables: EdaHostExecutable[]
286
+ }
287
+
288
+ /** Which EDA host is connected. */
289
+ export interface EdaHostIdentity {
290
+ hostType: EdaHostType
291
+ hostName: string
292
+ version: string
293
+ }
294
+
295
+ /**
296
+ * EDA-independent description of the connected host.
297
+ *
298
+ * Answering this message is the host's way of saying it is available — there is
299
+ * no `available` field to inspect.
300
+ */
301
+ export interface EdaHostInfo {
302
+ identity: EdaHostIdentity
303
+ installation: EdaHostInstallation
304
+ }