@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/README.md +61 -6
- package/lib/index.d.mts +106 -5
- package/lib/index.mjs +235 -46
- package/package.json +2 -2
- package/src/client.ts +217 -33
- package/src/config.ts +37 -0
- package/src/index.ts +53 -7
- package/src/tools.ts +118 -27
- package/src/types.ts +92 -0
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
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
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
|
|
82
|
-
`treat it as a failure.
|
|
83
|
-
|
|
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,
|
|
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
|
|
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
|
+
}
|