@huaqiu/dsh-tool-schematic-gen 0.1.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/LICENSE +21 -0
- package/README.md +8 -0
- package/cordis.patch.yml +7 -0
- package/lib/client.js +106004 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.mts +35 -0
- package/lib/index.mjs +1439 -0
- package/package.json +58 -0
- package/src/client/ecad.ts +147 -0
- package/src/client/hit-card.tsx +378 -0
- package/src/client/i18n.ts +148 -0
- package/src/client/index.ts +176 -0
- package/src/client/login-url.ts +91 -0
- package/src/client/parse.ts +153 -0
- package/src/client/progress.ts +161 -0
- package/src/client/stack-frame.tsx +269 -0
- package/src/client/theme.ts +228 -0
- package/src/client/trace-names.ts +308 -0
- package/src/client/trace-tree.ts +185 -0
- package/src/config.ts +203 -0
- package/src/index.ts +130 -0
- package/src/progress.ts +333 -0
- package/src/routes.ts +80 -0
- package/src/sse.ts +448 -0
- package/src/tools.ts +590 -0
- package/src/trace.ts +336 -0
package/src/trace.ts
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@huaqiu/dsh-tool-schematic-gen` — execution-trace model for the eda.cn
|
|
3
|
+
* design agents.
|
|
4
|
+
*
|
|
5
|
+
* Port of the `hq-eda-ai` schematic-gen trace contract
|
|
6
|
+
* (`packages/agents/schematic-gen/src/utils/trace.ts`) plus its frontend
|
|
7
|
+
* start/end pairing logic
|
|
8
|
+
* (`apps/web/src/components/sch_sub_gen/TraceTimeline.tsx#buildStatusEntries`).
|
|
9
|
+
*
|
|
10
|
+
* The eda.cn CopilotKit endpoint streams AG-UI `CUSTOM` events, each carrying
|
|
11
|
+
* one `TraceEvent` (LangGraph `custom` stream mode):
|
|
12
|
+
*
|
|
13
|
+
* data: {"type":"CUSTOM","name":"SCHEMATIC_GENERATOR_TRACE",
|
|
14
|
+
* "value":{"kind":"tool","phase":"start","scope":"schematicDesign",
|
|
15
|
+
* "name":"search_parts","ts":1770000000000}}
|
|
16
|
+
*
|
|
17
|
+
* `scope` is a `parent>child` breadcrumb and is the ONLY hierarchical signal
|
|
18
|
+
* the backend gives us — it is what the card turns into a call stack.
|
|
19
|
+
*
|
|
20
|
+
* @module @huaqiu/dsh-tool-schematic-gen
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// ── Wire model ─────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export type TracePhase = 'start' | 'end'
|
|
26
|
+
|
|
27
|
+
/** A LangGraph graph node entering/leaving. */
|
|
28
|
+
export interface NodeTraceEvent {
|
|
29
|
+
kind: 'node'
|
|
30
|
+
phase: TracePhase
|
|
31
|
+
/** Graph node name. */
|
|
32
|
+
node: string
|
|
33
|
+
ts: number
|
|
34
|
+
/**
|
|
35
|
+
* Full scope path (`parent>this`). Older backend builds omit it; the
|
|
36
|
+
* frontend then degrades to the node's own name (see {@link tracePath}).
|
|
37
|
+
*/
|
|
38
|
+
scope?: string
|
|
39
|
+
/** Optional human note emitted by the node. */
|
|
40
|
+
note?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A tool call entering/leaving. */
|
|
44
|
+
export interface ToolTraceEvent {
|
|
45
|
+
kind: 'tool'
|
|
46
|
+
phase: TracePhase
|
|
47
|
+
/** Scope path of the CALLER (`parent`), not including `name`. */
|
|
48
|
+
scope: string
|
|
49
|
+
name: string
|
|
50
|
+
ts: number
|
|
51
|
+
/**
|
|
52
|
+
* Instance path (`parent>leaf::task:<toolCallId>`), present only for spans
|
|
53
|
+
* synthesized from the AG-UI tool-call lifecycle. The hidden `::task:` suffix
|
|
54
|
+
* makes repeated calls of the SAME tool distinct so retries are not lost,
|
|
55
|
+
* while {@link traceName} keeps the display name clean.
|
|
56
|
+
*/
|
|
57
|
+
path?: string
|
|
58
|
+
/** Absent means success; only `false` marks failure. */
|
|
59
|
+
ok?: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type TraceEvent = NodeTraceEvent | ToolTraceEvent
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Custom-event names the eda.cn agents are known to use. Matching is
|
|
66
|
+
* deliberately tolerant: any name ending in `_TRACE` is accepted too, so a
|
|
67
|
+
* backend rename does not silently kill progress.
|
|
68
|
+
*/
|
|
69
|
+
export const KNOWN_TRACE_EVENT_NAMES: readonly string[] = [
|
|
70
|
+
'SCHEMATIC_GENERATOR_TRACE',
|
|
71
|
+
'SCHEMAGEN_TRACE',
|
|
72
|
+
'MODULAR_CIRCUIT_TRACE',
|
|
73
|
+
'MODULE_GEN_TRACE',
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
export function isTraceEventName(name: unknown): boolean {
|
|
77
|
+
if (typeof name !== 'string' || name.length === 0) return false
|
|
78
|
+
if (KNOWN_TRACE_EVENT_NAMES.includes(name)) return true
|
|
79
|
+
return /_TRACE$/i.test(name)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Parsing ────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
85
|
+
return !!v && typeof v === 'object' && !Array.isArray(v)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Validate one decoded `CUSTOM` payload as a {@link TraceEvent}.
|
|
90
|
+
* Returns `null` for anything else (interrupts, future event shapes).
|
|
91
|
+
*/
|
|
92
|
+
export function parseTraceEvent(value: unknown): TraceEvent | null {
|
|
93
|
+
if (!isRecord(value)) return null
|
|
94
|
+
const kind = value['kind']
|
|
95
|
+
const phase = value['phase']
|
|
96
|
+
const ts = value['ts']
|
|
97
|
+
if (phase !== 'start' && phase !== 'end') return null
|
|
98
|
+
const timestamp = typeof ts === 'number' && Number.isFinite(ts) ? ts : Date.now()
|
|
99
|
+
|
|
100
|
+
if (kind === 'node') {
|
|
101
|
+
const node = value['node']
|
|
102
|
+
if (typeof node !== 'string' || node.length === 0) return null
|
|
103
|
+
const ev: NodeTraceEvent = { kind: 'node', phase, node, ts: timestamp }
|
|
104
|
+
const scope = value['scope']
|
|
105
|
+
if (typeof scope === 'string' && scope.length > 0) ev.scope = scope
|
|
106
|
+
const note = value['note']
|
|
107
|
+
if (typeof note === 'string' && note.length > 0) ev.note = note
|
|
108
|
+
return ev
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (kind === 'tool') {
|
|
112
|
+
const name = value['name']
|
|
113
|
+
if (typeof name !== 'string' || name.length === 0) return null
|
|
114
|
+
const scope = value['scope']
|
|
115
|
+
const ev: ToolTraceEvent = {
|
|
116
|
+
kind: 'tool',
|
|
117
|
+
phase,
|
|
118
|
+
// Tolerate a missing scope by degrading to the tool's own name.
|
|
119
|
+
scope: typeof scope === 'string' && scope.length > 0 ? scope : name,
|
|
120
|
+
name,
|
|
121
|
+
ts: timestamp,
|
|
122
|
+
}
|
|
123
|
+
const path = value['path']
|
|
124
|
+
if (typeof path === 'string' && path.length > 0) ev.path = path
|
|
125
|
+
if (value['ok'] === false) ev.ok = false
|
|
126
|
+
return ev
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Collect every {@link TraceEvent} inside one `CUSTOM` payload. The backend
|
|
134
|
+
* normally writes one event per chunk, but array payloads are accepted so a
|
|
135
|
+
* batched writer cannot drop the stream.
|
|
136
|
+
*/
|
|
137
|
+
export function collectTraceEvents(value: unknown): TraceEvent[] {
|
|
138
|
+
if (Array.isArray(value)) {
|
|
139
|
+
const out: TraceEvent[] = []
|
|
140
|
+
for (const item of value) {
|
|
141
|
+
const ev = parseTraceEvent(item)
|
|
142
|
+
if (ev) out.push(ev)
|
|
143
|
+
}
|
|
144
|
+
return out
|
|
145
|
+
}
|
|
146
|
+
const ev = parseTraceEvent(value)
|
|
147
|
+
return ev ? [ev] : []
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Path derivation ────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Canonical `a>b>c` path for one event — the breadcrumb the card nests by.
|
|
154
|
+
*
|
|
155
|
+
* Mirrors `TraceTimeline.buildStatusEntries`: a node's path is its own scope
|
|
156
|
+
* (falling back to its name when the backend omits scope), while a tool's path
|
|
157
|
+
* is its caller scope with the tool name appended.
|
|
158
|
+
*/
|
|
159
|
+
export function tracePath(ev: TraceEvent): string {
|
|
160
|
+
if (ev.kind === 'node') {
|
|
161
|
+
return ev.scope && ev.scope.length > 0 ? ev.scope : ev.node
|
|
162
|
+
}
|
|
163
|
+
// The instance path wins when present: it is the only breadcrumb that
|
|
164
|
+
// distinguishes two calls of the same tool at the same scope.
|
|
165
|
+
if (ev.path && ev.path.length > 0) return ev.path
|
|
166
|
+
return ev.scope === ev.name ? ev.name : `${ev.scope}>${ev.name}`
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Display name of one event (`node:foo` for nodes, bare name for tools). */
|
|
170
|
+
export function traceName(ev: TraceEvent): string {
|
|
171
|
+
return ev.kind === 'node' ? `node:${ev.node}` : ev.name
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Start/end pairing ──────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
export type TraceStatus = 'running' | 'finished' | 'failed'
|
|
177
|
+
|
|
178
|
+
/** One paired span: a stack frame the card can render. */
|
|
179
|
+
export interface TraceFrame {
|
|
180
|
+
/** Stable synthetic id (`trace-<seq>`). */
|
|
181
|
+
id: string
|
|
182
|
+
/** Display key, e.g. `node:plan` or `search_parts`. */
|
|
183
|
+
name: string
|
|
184
|
+
/** Full `parent>child>leaf` breadcrumb used to nest the frame. */
|
|
185
|
+
path: string
|
|
186
|
+
status: TraceStatus
|
|
187
|
+
startedAt: number
|
|
188
|
+
finishedAt?: number
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Pair `start`/`end` events into {@link TraceFrame}s, preserving nesting via
|
|
193
|
+
* `path`.
|
|
194
|
+
*
|
|
195
|
+
* Uses the same per-key open stack as `buildStatusEntries`, so nested calls
|
|
196
|
+
* with the same name (a tool re-entered inside its own subtree) close in LIFO
|
|
197
|
+
* order instead of cross-contaminating.
|
|
198
|
+
*/
|
|
199
|
+
export function pairTraceEvents(events: readonly TraceEvent[]): TraceFrame[] {
|
|
200
|
+
const frames: TraceFrame[] = []
|
|
201
|
+
const open = new Map<string, number[]>()
|
|
202
|
+
let seq = 0
|
|
203
|
+
|
|
204
|
+
for (const ev of events) {
|
|
205
|
+
const name = traceName(ev)
|
|
206
|
+
const path = tracePath(ev)
|
|
207
|
+
// Key on the display name + path so two different subtrees using the same
|
|
208
|
+
// tool name never share an open slot.
|
|
209
|
+
const key = `${path}|${name}`
|
|
210
|
+
|
|
211
|
+
if (ev.phase === 'start') {
|
|
212
|
+
const stack = open.get(key)
|
|
213
|
+
const index = frames.length
|
|
214
|
+
if (stack) stack.push(index)
|
|
215
|
+
else open.set(key, [index])
|
|
216
|
+
frames.push({ id: `trace-${seq++}`, name, path, status: 'running', startedAt: ev.ts })
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const stack = open.get(key)
|
|
221
|
+
const index = stack?.pop()
|
|
222
|
+
if (index === undefined) continue // unpaired end — nothing to close
|
|
223
|
+
const failed = ev.kind === 'tool' && ev.ok === false
|
|
224
|
+
frames[index] = {
|
|
225
|
+
...frames[index]!,
|
|
226
|
+
status: failed ? 'failed' : 'finished',
|
|
227
|
+
finishedAt: ev.ts,
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return frames
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── AG-UI tool-call lifecycle ──────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Marker appended to the leaf of a synthesized instance path. It is invisible
|
|
238
|
+
* in the UI (stripped from display names and from the tree key) but makes each
|
|
239
|
+
* invocation of a repeated tool a distinct path.
|
|
240
|
+
*/
|
|
241
|
+
export const TASK_MARK_RE = /::task:[^>]*/g
|
|
242
|
+
|
|
243
|
+
/** Strip the hidden `::task:<id>` suffixes from one path segment/path. */
|
|
244
|
+
export function stripTaskIds(path: string): string {
|
|
245
|
+
return path.replace(TASK_MARK_RE, '')
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** True when a path was synthesized from the tool-call lifecycle. */
|
|
249
|
+
export function hasTaskId(path: string): boolean {
|
|
250
|
+
return path.includes('::task:')
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
interface ActiveToolCall {
|
|
254
|
+
semanticPath: string
|
|
255
|
+
instancePath: string
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Convert the **standard AG-UI tool-call lifecycle** (`TOOL_CALL_START` /
|
|
260
|
+
* `TOOL_CALL_END`) into {@link TraceEvent}s.
|
|
261
|
+
*
|
|
262
|
+
* This exists because the two eda.cn agents report progress differently:
|
|
263
|
+
*
|
|
264
|
+
* - `schemagen` (schematic) emits LangGraph `custom`-stream `TraceEvent`s,
|
|
265
|
+
* republished as AG-UI `CUSTOM` events named `SCHEMATIC_GENERATOR_TRACE`.
|
|
266
|
+
* - `modular_circuit` (system design) does NOT emit those. Its stack is
|
|
267
|
+
* rebuilt by the agent server from LangGraph *tasks* and published as the
|
|
268
|
+
* ordinary AG-UI `TOOL_CALL_START` / `TOOL_CALL_END` pair — see
|
|
269
|
+
* `ModuleGenTraceProvider.onToolCallStartEvent` in hq-eda-ai.
|
|
270
|
+
*
|
|
271
|
+
* Without this adapter the system-design tool reported no progress at all.
|
|
272
|
+
*
|
|
273
|
+
* Port of `createToolInstancePath` + `toToolTraceEvent` in
|
|
274
|
+
* `apps/web/src/lib/modular_circuit/context/ModuleGenTraceProvider.tsx`.
|
|
275
|
+
*/
|
|
276
|
+
export class ToolCallTracker {
|
|
277
|
+
private readonly active = new Map<string, ActiveToolCall>()
|
|
278
|
+
|
|
279
|
+
/** Forget every open call — invoke on `RUN_STARTED`. */
|
|
280
|
+
reset(): void {
|
|
281
|
+
this.active.clear()
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Build an instance path, reusing the deepest currently-open call whose
|
|
286
|
+
* semantic path is a prefix of this one so nested tools nest properly.
|
|
287
|
+
*/
|
|
288
|
+
private instancePath(semanticPath: string, toolCallId: string): string {
|
|
289
|
+
let parent: ActiveToolCall | undefined
|
|
290
|
+
for (const candidate of this.active.values()) {
|
|
291
|
+
if (!semanticPath.startsWith(`${candidate.semanticPath}>`)) continue
|
|
292
|
+
if (!parent || candidate.semanticPath.length > parent.semanticPath.length) {
|
|
293
|
+
parent = candidate
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const relative = parent ? semanticPath.slice(parent.semanticPath.length + 1) : semanticPath
|
|
297
|
+
const segments = relative.split('>').map((s) => s.trim()).filter((s) => s.length > 0)
|
|
298
|
+
const leaf = segments.length - 1
|
|
299
|
+
if (leaf >= 0) segments[leaf] = `${segments[leaf]}::task:${toolCallId}`
|
|
300
|
+
const current = segments.join('>')
|
|
301
|
+
return parent ? `${parent.instancePath}>${current}` : current
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Split a semantic path into its parent scope and leaf display name. */
|
|
305
|
+
private static split(semanticPath: string): { scope: string; name: string } {
|
|
306
|
+
const segments = semanticPath.split('>').map((s) => s.trim()).filter((s) => s.length > 0)
|
|
307
|
+
const name = segments[segments.length - 1] || 'unknown'
|
|
308
|
+
const scope = segments.length > 1 ? segments.slice(0, -1).join('>') : name
|
|
309
|
+
return { scope, name }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Open a tool call. Returns the `start` trace event. */
|
|
313
|
+
start(toolCallId: string, semanticPath: string, ts: number = Date.now()): TraceEvent {
|
|
314
|
+
const instancePath = this.instancePath(semanticPath, toolCallId)
|
|
315
|
+
if (toolCallId) this.active.set(toolCallId, { semanticPath, instancePath })
|
|
316
|
+
const { scope, name } = ToolCallTracker.split(semanticPath)
|
|
317
|
+
return { kind: 'tool', phase: 'start', scope, name, path: instancePath, ts }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Close a tool call. Returns the `end` trace event, or `null` when the id
|
|
322
|
+
* was never opened (a late/duplicate frame we must not invent a span for).
|
|
323
|
+
*/
|
|
324
|
+
end(toolCallId: string, ts: number = Date.now()): TraceEvent | null {
|
|
325
|
+
const known = this.active.get(toolCallId)
|
|
326
|
+
if (!known) return null
|
|
327
|
+
this.active.delete(toolCallId)
|
|
328
|
+
const { scope, name } = ToolCallTracker.split(known.semanticPath)
|
|
329
|
+
return { kind: 'tool', phase: 'end', scope, name, path: known.instancePath, ts, ok: true }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Ids still open. Exposed for diagnostics/tests. */
|
|
333
|
+
get openIds(): string[] {
|
|
334
|
+
return [...this.active.keys()]
|
|
335
|
+
}
|
|
336
|
+
}
|