@oqtopus-team/pi-qdash 0.1.2 → 0.1.4
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 +1 -0
- package/extensions/lib/figure-analysis.ts +112 -0
- package/extensions/lib/figures.ts +78 -0
- package/extensions/lib/forum-render.ts +152 -0
- package/extensions/lib/links.ts +57 -0
- package/extensions/lib/payload.ts +70 -0
- package/extensions/lib/render.ts +110 -0
- package/extensions/lib/results.ts +30 -0
- package/extensions/lib/timeseries-plot.ts +101 -0
- package/extensions/lib/wiring-analysis.ts +169 -0
- package/extensions/lib/write-gate.ts +35 -0
- package/extensions/qdash.ts +164 -832
- package/package.json +1 -1
- package/skills/qdash/SKILL.md +2 -1
package/extensions/qdash.ts
CHANGED
|
@@ -5,9 +5,20 @@ import { homedir } from "node:os";
|
|
|
5
5
|
|
|
6
6
|
import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { QDashClient, defaultConfigPath, type DownloadedFile, type TaskResultFigureOptions } from "@oqtopus-team/qdash-client";
|
|
8
|
-
import {
|
|
8
|
+
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import { Type } from "typebox";
|
|
10
10
|
|
|
11
|
+
import { analyzePlotlyFigure, figureAnalysisText } from "./lib/figure-analysis.js";
|
|
12
|
+
import { fetchFigureDetails, figureComponent, figureResultText, mediaTypeForPath, type FigureDetails } from "./lib/figures.js";
|
|
13
|
+
import { forumDetailComponent, forumDetailLines, forumListLines, forumPostTitle, forumPostsFromPayload } from "./lib/forum-render.js";
|
|
14
|
+
import { qdashObjectLinks, qdashWebBaseUrl, qdashWebUrl, safeConfig, withQDashLinks } from "./lib/links.js";
|
|
15
|
+
import { arrayFromPayload, compactItems, firstNumber, firstString, formatItem, payloadTotal, statusIcon } from "./lib/payload.js";
|
|
16
|
+
import { toTextToolResult, toToolResult } from "./lib/results.js";
|
|
17
|
+
import { ansi, boxLinesToWidth, boxed, formatNumber, textComponent } from "./lib/render.js";
|
|
18
|
+
import { plotSeriesLines, timeseriesPlotComponent, timeseriesPoints } from "./lib/timeseries-plot.js";
|
|
19
|
+
import { installQDashWriteGate } from "./lib/write-gate.js";
|
|
20
|
+
import { analyzeWiringMarkdown, wiringInsightLines, type WiringInsights } from "./lib/wiring-analysis.js";
|
|
21
|
+
|
|
11
22
|
type QDashQueryParams = {
|
|
12
23
|
profile?: string;
|
|
13
24
|
configPath?: string;
|
|
@@ -111,18 +122,6 @@ type ConfirmableParams = {
|
|
|
111
122
|
confirmWrite?: boolean;
|
|
112
123
|
};
|
|
113
124
|
|
|
114
|
-
type FigureDetails = {
|
|
115
|
-
tool: string;
|
|
116
|
-
path: string;
|
|
117
|
-
mediaType: string;
|
|
118
|
-
sizeBytes: number;
|
|
119
|
-
base64?: string;
|
|
120
|
-
text?: string;
|
|
121
|
-
taskId?: string;
|
|
122
|
-
figurePaths?: string[];
|
|
123
|
-
jsonFigurePaths?: string[];
|
|
124
|
-
};
|
|
125
|
-
|
|
126
125
|
type QDashContextState = {
|
|
127
126
|
profile?: string;
|
|
128
127
|
chipId?: string;
|
|
@@ -136,17 +135,6 @@ type QDashContextState = {
|
|
|
136
135
|
|
|
137
136
|
const CONTEXT_ENTRY_TYPE = "qdash-context";
|
|
138
137
|
const GLOBAL_CONTEXT_PATH = join(homedir(), ".pi", "agent", "qdash-context.json");
|
|
139
|
-
const WRITE_TOOL_NAMES = new Set([
|
|
140
|
-
"qdash_create_agent_session",
|
|
141
|
-
"qdash_submit_agent_action",
|
|
142
|
-
"qdash_commit_agent_candidate",
|
|
143
|
-
"qdash_execute_agent_action",
|
|
144
|
-
"qdash_commit_agent_campaign_candidates",
|
|
145
|
-
"qdash_apply_agent_candidate_commit",
|
|
146
|
-
"qdash_create_forum_post",
|
|
147
|
-
"qdash_update_forum_post",
|
|
148
|
-
"qdash_create_forum_evidence_reply",
|
|
149
|
-
]);
|
|
150
138
|
let currentContext: QDashContextState = {};
|
|
151
139
|
|
|
152
140
|
function applyQDashContext<T extends { profile?: string; chipId?: string; sessionId?: string }>(params: T): T {
|
|
@@ -405,6 +393,58 @@ type TaskResultFigureFile = DownloadedFile & {
|
|
|
405
393
|
jsonFigurePaths: string[];
|
|
406
394
|
};
|
|
407
395
|
|
|
396
|
+
type CalibrationEvidence = {
|
|
397
|
+
id?: string;
|
|
398
|
+
source: string;
|
|
399
|
+
target?: Record<string, unknown>;
|
|
400
|
+
task?: Record<string, unknown>;
|
|
401
|
+
metrics?: Record<string, number | string | boolean | null>;
|
|
402
|
+
parameters?: Record<string, number | string | boolean | null>;
|
|
403
|
+
resultJson?: unknown;
|
|
404
|
+
figures?: Array<Record<string, unknown>>;
|
|
405
|
+
logs?: string[];
|
|
406
|
+
notes?: string;
|
|
407
|
+
history?: Array<Record<string, unknown>>;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
function parameterValueRecord(value: unknown): Record<string, number | string | boolean | null> {
|
|
411
|
+
const output: Record<string, number | string | boolean | null> = {};
|
|
412
|
+
const input = value && typeof value === "object" ? value as Record<string, unknown> : {};
|
|
413
|
+
for (const [key, raw] of Object.entries(input)) {
|
|
414
|
+
const object = raw && typeof raw === "object" ? raw as Record<string, unknown> : undefined;
|
|
415
|
+
const candidate = object && "value" in object ? object.value : raw;
|
|
416
|
+
if (typeof candidate === "number" || typeof candidate === "string" || typeof candidate === "boolean" || candidate === null) output[key] = candidate;
|
|
417
|
+
}
|
|
418
|
+
return output;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function firstStringField(object: Record<string, unknown>, keys: string[]): string | undefined {
|
|
422
|
+
for (const key of keys) {
|
|
423
|
+
const value = object[key];
|
|
424
|
+
if (typeof value === "string" && value) return value;
|
|
425
|
+
}
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function qcalEvidenceText(evidence: CalibrationEvidence): string {
|
|
430
|
+
const metrics = Object.entries(evidence.metrics ?? {}).slice(0, 8).map(([key, value]) => ` - ${key}: ${String(value)}`);
|
|
431
|
+
const figures = evidence.figures ?? [];
|
|
432
|
+
return boxed("QDash QCal Evidence", [
|
|
433
|
+
`id ${evidence.id ?? "n/a"}`,
|
|
434
|
+
`source ${evidence.source}`,
|
|
435
|
+
`target ${evidence.target?.id ?? evidence.target?.couplingId ?? evidence.target?.qids ?? "n/a"}`,
|
|
436
|
+
`task ${evidence.task?.name ?? "n/a"} (${evidence.task?.status ?? "unknown"})`,
|
|
437
|
+
"",
|
|
438
|
+
"Metrics",
|
|
439
|
+
...(metrics.length ? metrics : [" - none"]),
|
|
440
|
+
"",
|
|
441
|
+
`Figures ${figures.length}`,
|
|
442
|
+
...figures.map((figure, index) => ` - #${index + 1} ${String(figure.role ?? "plot")} ${String(figure.path ?? figure.url ?? "")}`),
|
|
443
|
+
"",
|
|
444
|
+
"Use qcal_evaluate_bundle with details.evidence to evaluate this evidence with pi-qcal.",
|
|
445
|
+
]).join("\n");
|
|
446
|
+
}
|
|
447
|
+
|
|
408
448
|
function cleanQuery(values: Record<string, unknown> = {}): Record<string, unknown> {
|
|
409
449
|
return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined && value !== null));
|
|
410
450
|
}
|
|
@@ -448,23 +488,6 @@ type WiringInsightParams = CooldownWiringParams & {
|
|
|
448
488
|
color?: boolean;
|
|
449
489
|
};
|
|
450
490
|
|
|
451
|
-
type AttenuationEntry = {
|
|
452
|
-
mux: string;
|
|
453
|
-
line: "control" | "readout_send";
|
|
454
|
-
totalDb: number;
|
|
455
|
-
components: number[];
|
|
456
|
-
panel?: string;
|
|
457
|
-
hardwarePort?: string;
|
|
458
|
-
qid?: string;
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
type WiringInsights = {
|
|
462
|
-
entries: AttenuationEntry[];
|
|
463
|
-
control: { commonTotalDb?: number; anomalies: AttenuationEntry[]; totals: Record<string, number> };
|
|
464
|
-
readoutSend: { commonTotalDb?: number; anomalies: AttenuationEntry[]; totals: Record<string, number> };
|
|
465
|
-
suggestions: string[];
|
|
466
|
-
};
|
|
467
|
-
|
|
468
491
|
function newestCooldown(cooldowns: CooldownRecord[]): CooldownRecord | undefined {
|
|
469
492
|
return [...cooldowns].sort((a, b) => {
|
|
470
493
|
const activeDifference = Number(a.ended_at != null) - Number(b.ended_at != null);
|
|
@@ -473,145 +496,6 @@ function newestCooldown(cooldowns: CooldownRecord[]): CooldownRecord | undefined
|
|
|
473
496
|
})[0];
|
|
474
497
|
}
|
|
475
498
|
|
|
476
|
-
function markdownTableRows(markdown: string): string[][] {
|
|
477
|
-
return markdown.split("\n")
|
|
478
|
-
.map((line) => line.trim())
|
|
479
|
-
.filter((line) => line.startsWith("|") && line.endsWith("|"))
|
|
480
|
-
.map((line) => line.slice(1, -1).split("|").map((cell) => cell.trim()))
|
|
481
|
-
.filter((cells) => cells.length >= 7 && !cells.every((cell) => /^-+$/.test(cell.replace(/\s/g, ""))));
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function expandPanelSpec(value: string): string[] {
|
|
485
|
-
return value.split(/[、,]/).flatMap((part) => {
|
|
486
|
-
const token = part.trim();
|
|
487
|
-
if (!token) return [];
|
|
488
|
-
const range = token.match(/^([A-Za-z]+)(\d+)\s*-\s*(\d+)$/);
|
|
489
|
-
if (range) {
|
|
490
|
-
const [, prefix, startText, endText] = range;
|
|
491
|
-
const start = Number(startText);
|
|
492
|
-
const end = Number(endText);
|
|
493
|
-
const step = start <= end ? 1 : -1;
|
|
494
|
-
const panels: string[] = [];
|
|
495
|
-
for (let value = start; step > 0 ? value <= end : value >= end; value += step) panels.push(`${prefix}${value}`);
|
|
496
|
-
return panels;
|
|
497
|
-
}
|
|
498
|
-
return [token];
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function splitPortsAndPanels(cell: string): { ports: string[]; panels: string[] } {
|
|
503
|
-
const [portsText = cell, panelsText = ""] = cell.split(/\s+\/\s+/, 2);
|
|
504
|
-
return {
|
|
505
|
-
ports: portsText.split(/[、,]/).map((part) => part.trim()).filter(Boolean),
|
|
506
|
-
panels: expandPanelSpec(panelsText),
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function attenuationNumbers(text: string): number[] {
|
|
511
|
-
return [...text.matchAll(/(?:追加\s*)?(\d+(?:\.\d+)?)\s*dB/gi)].map((match) => Number(match[1])).filter(Number.isFinite);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function panelAttenuationTotals(cell: string): Array<{ panels?: string[]; components: number[]; totalDb: number }> {
|
|
515
|
-
const parenthesized = [...cell.matchAll(/([^()]+)(([^)]+))/g)].map((match) => {
|
|
516
|
-
const components = attenuationNumbers(match[1]);
|
|
517
|
-
return { panels: expandPanelSpec(match[2]), components, totalDb: components.reduce((sum, value) => sum + value, 0) };
|
|
518
|
-
}).filter((segment) => segment.components.length > 0);
|
|
519
|
-
if (parenthesized.length > 0) return parenthesized;
|
|
520
|
-
const components = attenuationNumbers(cell);
|
|
521
|
-
return components.length > 0 ? [{ components, totalDb: components.reduce((sum, value) => sum + value, 0) }] : [];
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function mostCommonTotal(entries: AttenuationEntry[]): number | undefined {
|
|
525
|
-
const counts = new Map<number, number>();
|
|
526
|
-
for (const entry of entries) counts.set(entry.totalDb, (counts.get(entry.totalDb) ?? 0) + 1);
|
|
527
|
-
return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0] - b[0])[0]?.[0];
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
function totalHistogram(entries: AttenuationEntry[]): Record<string, number> {
|
|
531
|
-
const histogram: Record<string, number> = {};
|
|
532
|
-
for (const entry of entries) histogram[String(entry.totalDb)] = (histogram[String(entry.totalDb)] ?? 0) + 1;
|
|
533
|
-
return histogram;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function analyzeWiringMarkdown(markdown: string | undefined): WiringInsights | undefined {
|
|
537
|
-
if (!markdown) return undefined;
|
|
538
|
-
const rows = markdownTableRows(markdown);
|
|
539
|
-
const dataRows = rows.filter((cells) => /^\d+$/.test(cells[0]));
|
|
540
|
-
const entries: AttenuationEntry[] = [];
|
|
541
|
-
for (const cells of dataRows) {
|
|
542
|
-
const mux = cells[0].padStart(2, "0");
|
|
543
|
-
const muxNumber = Number(cells[0]);
|
|
544
|
-
const control = splitPortsAndPanels(cells[1] ?? "");
|
|
545
|
-
const controlSegments = panelAttenuationTotals(cells[5] ?? "");
|
|
546
|
-
for (let index = 0; index < control.panels.length; index++) {
|
|
547
|
-
const panel = control.panels[index];
|
|
548
|
-
const segment = controlSegments.find((item) => item.panels?.includes(panel)) ?? (controlSegments.length === 1 ? controlSegments[0] : undefined);
|
|
549
|
-
if (!segment) continue;
|
|
550
|
-
entries.push({
|
|
551
|
-
mux,
|
|
552
|
-
line: "control",
|
|
553
|
-
panel,
|
|
554
|
-
hardwarePort: control.ports[index],
|
|
555
|
-
qid: Number.isFinite(muxNumber) ? String(muxNumber * 4 + index) : undefined,
|
|
556
|
-
totalDb: segment.totalDb,
|
|
557
|
-
components: segment.components,
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
const readoutSegments = panelAttenuationTotals(cells[6] ?? "");
|
|
561
|
-
const readout = splitPortsAndPanels(cells[2] ?? "");
|
|
562
|
-
if (readoutSegments[0]) {
|
|
563
|
-
entries.push({
|
|
564
|
-
mux,
|
|
565
|
-
line: "readout_send",
|
|
566
|
-
panel: readout.panels[0],
|
|
567
|
-
hardwarePort: readout.ports[0],
|
|
568
|
-
totalDb: readoutSegments[0].totalDb,
|
|
569
|
-
components: readoutSegments[0].components,
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
const controlEntries = entries.filter((entry) => entry.line === "control");
|
|
574
|
-
const readoutEntries = entries.filter((entry) => entry.line === "readout_send");
|
|
575
|
-
const controlCommon = mostCommonTotal(controlEntries);
|
|
576
|
-
const readoutCommon = mostCommonTotal(readoutEntries);
|
|
577
|
-
const controlAnomalies = controlCommon === undefined ? [] : controlEntries.filter((entry) => entry.totalDb !== controlCommon);
|
|
578
|
-
const readoutAnomalies = readoutCommon === undefined ? [] : readoutEntries.filter((entry) => entry.totalDb !== readoutCommon);
|
|
579
|
-
const suggestions = [
|
|
580
|
-
controlCommon !== undefined ? `Control の最頻総減衰量は ${controlCommon} dB です。` : undefined,
|
|
581
|
-
controlAnomalies.length > 0
|
|
582
|
-
? `Control で最頻値と異なるポートが ${controlAnomalies.length} 件あります: ${controlAnomalies.map((entry) => entry.qid ? `q${entry.qid}` : `${entry.mux}/${entry.panel}`).join(", ")}`
|
|
583
|
-
: controlEntries.length > 0 ? "Control の総減衰量は全ポートで揃っています。" : undefined,
|
|
584
|
-
readoutCommon !== undefined ? `Readout send の最頻総減衰量は ${readoutCommon} dB です。` : undefined,
|
|
585
|
-
readoutAnomalies.length > 0
|
|
586
|
-
? `Readout send で最頻値と異なる MUX が ${readoutAnomalies.length} 件あります: ${readoutAnomalies.map((entry) => `MUX${entry.mux}`).join(", ")}`
|
|
587
|
-
: readoutEntries.length > 0 ? "Readout send の総減衰量は全 MUX で揃っています。" : undefined,
|
|
588
|
-
].filter((line): line is string => Boolean(line));
|
|
589
|
-
return {
|
|
590
|
-
entries,
|
|
591
|
-
control: { commonTotalDb: controlCommon, anomalies: controlAnomalies, totals: totalHistogram(controlEntries) },
|
|
592
|
-
readoutSend: { commonTotalDb: readoutCommon, anomalies: readoutAnomalies, totals: totalHistogram(readoutEntries) },
|
|
593
|
-
suggestions,
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
function wiringInsightLines(insights: WiringInsights, color = false): string[] {
|
|
598
|
-
const accent = (text: string) => color ? ansi("1;36", text) : text;
|
|
599
|
-
const muted = (text: string) => color ? ansi("90", text) : text;
|
|
600
|
-
const warning = (text: string) => color ? ansi("33", text) : text;
|
|
601
|
-
const formatEntry = (entry: AttenuationEntry) => `${entry.qid ? `q${entry.qid}` : `MUX${entry.mux}`} ${muted(`MUX${entry.mux}`)} ${entry.panel ?? ""}${entry.hardwarePort ? `/${entry.hardwarePort}` : ""}: ${warning(`${entry.totalDb} dB`)} (${entry.components.join("+")})`;
|
|
602
|
-
return boxed("QDash Wiring Insights", [
|
|
603
|
-
...insights.suggestions.map((line) => `• ${line}`),
|
|
604
|
-
"",
|
|
605
|
-
accent("Control totals"),
|
|
606
|
-
` ${Object.entries(insights.control.totals).map(([total, count]) => `${total} dB × ${count}`).join(", ") || muted("none")}`,
|
|
607
|
-
...(insights.control.anomalies.length > 0 ? ["", accent("Control anomalies"), ...insights.control.anomalies.map((entry) => ` ${formatEntry(entry)}`)] : []),
|
|
608
|
-
"",
|
|
609
|
-
accent("Readout send totals"),
|
|
610
|
-
` ${Object.entries(insights.readoutSend.totals).map(([total, count]) => `${total} dB × ${count}`).join(", ") || muted("none")}`,
|
|
611
|
-
...(insights.readoutSend.anomalies.length > 0 ? ["", accent("Readout send anomalies"), ...insights.readoutSend.anomalies.map((entry) => ` ${formatEntry(entry)}`)] : []),
|
|
612
|
-
], color);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
499
|
async function resolveCooldownWiring(params: CooldownWiringParams) {
|
|
616
500
|
params = applyQDashContext(params);
|
|
617
501
|
const client = await makeClient(params);
|
|
@@ -682,84 +566,6 @@ async function resolveCooldownWiring(params: CooldownWiringParams) {
|
|
|
682
566
|
};
|
|
683
567
|
}
|
|
684
568
|
|
|
685
|
-
function qdashWebBaseUrl(client: QDashClient): string {
|
|
686
|
-
return client.config.baseUrl.replace(/\/api\/?$/, "").replace(/\/$/, "");
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
function qdashWebUrl(client: QDashClient, path: string): string {
|
|
690
|
-
return `${qdashWebBaseUrl(client)}${path.startsWith("/") ? path : `/${path}`}`;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
function qdashObjectLinks(client: QDashClient, object: Record<string, unknown>): Record<string, string> {
|
|
694
|
-
const links: Record<string, string> = {};
|
|
695
|
-
const taskId = firstString(object, ["task_id", "taskId"]);
|
|
696
|
-
const executionId = firstString(object, ["execution_id", "executionId"]);
|
|
697
|
-
const postId = firstString(object, ["post_id", "forum_post_id", "id"]);
|
|
698
|
-
const issueId = firstString(object, ["issue_id"]);
|
|
699
|
-
const sessionId = firstString(object, ["session_id", "sessionId"]);
|
|
700
|
-
if (taskId) links.task_result = qdashWebUrl(client, `/task-results/${encodeURIComponent(taskId)}`);
|
|
701
|
-
if (executionId) links.execution = qdashWebUrl(client, `/executions/${encodeURIComponent(executionId)}`);
|
|
702
|
-
if (postId) links.forum_post = qdashWebUrl(client, `/forum/posts/${encodeURIComponent(postId)}`);
|
|
703
|
-
if (issueId) links.issue = qdashWebUrl(client, `/issues/${encodeURIComponent(issueId)}`);
|
|
704
|
-
if (sessionId) links.agent_session = qdashWebUrl(client, `/agent-sessions/${encodeURIComponent(sessionId)}`);
|
|
705
|
-
return links;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
function withQDashLinks(client: QDashClient, data: unknown): unknown {
|
|
709
|
-
if (!data || typeof data !== "object" || Array.isArray(data)) return data;
|
|
710
|
-
const object = data as Record<string, unknown>;
|
|
711
|
-
const links = qdashObjectLinks(client, object);
|
|
712
|
-
if (Object.keys(links).length === 0) return data;
|
|
713
|
-
return { ...object, _links: links };
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
function safeConfig(client: QDashClient, source: string) {
|
|
717
|
-
const config = client.config;
|
|
718
|
-
return {
|
|
719
|
-
source,
|
|
720
|
-
baseUrl: config.baseUrl,
|
|
721
|
-
projectId: config.projectId ?? null,
|
|
722
|
-
timeoutSeconds: config.timeoutSeconds,
|
|
723
|
-
verifyTls: config.verifyTls,
|
|
724
|
-
proxyConfigured: Boolean(config.proxy),
|
|
725
|
-
apiTokenConfigured: Boolean(config.apiToken),
|
|
726
|
-
cloudflareAccessConfigured: Boolean(config.cfAccessClientId || config.cfAccessClientSecret),
|
|
727
|
-
userAgent: config.userAgent,
|
|
728
|
-
retry: config.retry,
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
function redact(value: unknown): unknown {
|
|
733
|
-
if (Array.isArray(value)) return value.map(redact);
|
|
734
|
-
if (value && typeof value === "object") {
|
|
735
|
-
return Object.fromEntries(
|
|
736
|
-
Object.entries(value as Record<string, unknown>).map(([key, item]) => {
|
|
737
|
-
if (/token|password|secret|authorization|api[_-]?key/i.test(key)) return [key, "[redacted]"];
|
|
738
|
-
return [key, redact(item)];
|
|
739
|
-
}),
|
|
740
|
-
);
|
|
741
|
-
}
|
|
742
|
-
return value;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
function toToolResult(data: unknown, details: Record<string, unknown> = {}) {
|
|
746
|
-
const safeData = redact(data);
|
|
747
|
-
let text = JSON.stringify(safeData, null, 2);
|
|
748
|
-
if (text.length > 20_000) text = `${text.slice(0, 20_000)}\n... [truncated]`;
|
|
749
|
-
return {
|
|
750
|
-
content: [{ type: "text" as const, text }],
|
|
751
|
-
details: { ...details, data: safeData },
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
function toTextToolResult(text: string, data: unknown, details: Record<string, unknown> = {}) {
|
|
756
|
-
const safeData = redact(data);
|
|
757
|
-
return {
|
|
758
|
-
content: [{ type: "text" as const, text }],
|
|
759
|
-
details: { ...details, data: safeData },
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
|
|
763
569
|
function configProfiles(configPath = defaultConfigPath()): { path: string; exists: boolean; profiles: string[] } {
|
|
764
570
|
if (!existsSync(configPath)) return { path: configPath, exists: false, profiles: [] };
|
|
765
571
|
const contents = readFileSync(configPath, "utf8");
|
|
@@ -767,64 +573,6 @@ function configProfiles(configPath = defaultConfigPath()): { path: string; exist
|
|
|
767
573
|
return { path: configPath, exists: true, profiles };
|
|
768
574
|
}
|
|
769
575
|
|
|
770
|
-
function arrayFromPayload(payload: unknown): unknown[] {
|
|
771
|
-
if (Array.isArray(payload)) return payload;
|
|
772
|
-
if (payload && typeof payload === "object") {
|
|
773
|
-
const object = payload as Record<string, unknown>;
|
|
774
|
-
for (const key of ["items", "results", "data", "issues", "executions", "chips", "tasks", "insights"]) {
|
|
775
|
-
if (Array.isArray(object[key])) return object[key];
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
return [];
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
function compactItems(payload: unknown, keys: string[], limit: number): Record<string, unknown>[] {
|
|
782
|
-
return arrayFromPayload(payload).slice(0, limit).map((item) => {
|
|
783
|
-
if (!item || typeof item !== "object") return { value: item };
|
|
784
|
-
const object = item as Record<string, unknown>;
|
|
785
|
-
return Object.fromEntries(keys.filter((key) => key in object).map((key) => [key, object[key]]));
|
|
786
|
-
});
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
function payloadTotal(payload: unknown): number {
|
|
790
|
-
const fallback = arrayFromPayload(payload).length;
|
|
791
|
-
if (!payload || typeof payload !== "object") return fallback;
|
|
792
|
-
const object = payload as Record<string, unknown>;
|
|
793
|
-
for (const key of ["total", "total_count", "count", "total_items"]) {
|
|
794
|
-
const value = object[key];
|
|
795
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
796
|
-
}
|
|
797
|
-
return fallback;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
function firstString(object: Record<string, unknown>, keys: string[]): string | undefined {
|
|
801
|
-
for (const key of keys) {
|
|
802
|
-
const value = object[key];
|
|
803
|
-
if (typeof value === "string" && value.length > 0) return value;
|
|
804
|
-
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
805
|
-
}
|
|
806
|
-
return undefined;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
function statusIcon(status: string | undefined): string {
|
|
810
|
-
const normalized = status?.toLowerCase() ?? "";
|
|
811
|
-
if (["completed", "success", "succeeded", "active", "applied"].includes(normalized)) return "✓";
|
|
812
|
-
if (["failed", "error", "crashed"].includes(normalized)) return "✗";
|
|
813
|
-
if (["running", "pending", "queued", "in_progress"].includes(normalized)) return "…";
|
|
814
|
-
if (["cancelled", "canceled"].includes(normalized)) return "-";
|
|
815
|
-
return "•";
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
function formatItem(item: Record<string, unknown>, fallbackId: string): string {
|
|
819
|
-
const id = firstString(item, ["issue_id", "execution_id", "flow_run_id", "task_id", "id"]) ?? fallbackId;
|
|
820
|
-
const title = firstString(item, ["title", "task_name", "flow_name", "name"]);
|
|
821
|
-
const status = firstString(item, ["status", "execution_status", "activity_status"]);
|
|
822
|
-
const target = firstString(item, ["qid", "coupling_id"]);
|
|
823
|
-
return [statusIcon(status), shortId(id, 14), title, target ? `(${target})` : undefined, status ? `[${status}]` : undefined]
|
|
824
|
-
.filter(Boolean)
|
|
825
|
-
.join(" ");
|
|
826
|
-
}
|
|
827
|
-
|
|
828
576
|
async function buildDashboard(params: { profile?: string; configPath?: string; useEnv?: boolean; chipId?: string; limit?: number }) {
|
|
829
577
|
params = applyQDashContext(params);
|
|
830
578
|
const limit = params.limit ?? 5;
|
|
@@ -866,58 +614,6 @@ async function buildDashboard(params: { profile?: string; configPath?: string; u
|
|
|
866
614
|
};
|
|
867
615
|
}
|
|
868
616
|
|
|
869
|
-
function ansi(code: string, text: string): string {
|
|
870
|
-
return `\u001b[${code}m${text}\u001b[0m`;
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
function stripAnsi(text: string): string {
|
|
874
|
-
return text.replace(/\u001b\[[0-9;]*m/g, "");
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
function displayWidth(text: string): number {
|
|
878
|
-
return visibleWidth(stripAnsi(text));
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
function padAnsi(text: string, width: number): string {
|
|
882
|
-
return text + " ".repeat(Math.max(0, width - displayWidth(text)));
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
function truncateDisplay(text: string, width: number): string {
|
|
886
|
-
if (displayWidth(text) <= width) return text;
|
|
887
|
-
const ellipsis = "…";
|
|
888
|
-
const target = Math.max(0, width - visibleWidth(ellipsis));
|
|
889
|
-
let out = "";
|
|
890
|
-
let used = 0;
|
|
891
|
-
for (const char of Array.from(text)) {
|
|
892
|
-
const charWidth = visibleWidth(char);
|
|
893
|
-
if (used + charWidth > target) break;
|
|
894
|
-
out += char;
|
|
895
|
-
used += charWidth;
|
|
896
|
-
}
|
|
897
|
-
return out + ellipsis;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
function boxed(title: string, body: string[], color = false): string[] {
|
|
901
|
-
const plainTitle = ` ${title} `;
|
|
902
|
-
const contentWidth = Math.min(
|
|
903
|
-
92,
|
|
904
|
-
Math.max(36, ...body.map((line) => displayWidth(line)), visibleWidth(plainTitle)),
|
|
905
|
-
);
|
|
906
|
-
const innerWidth = contentWidth + 2;
|
|
907
|
-
const borderColor = (text: string) => color ? ansi("90", text) : text;
|
|
908
|
-
const titleText = color ? ansi("1;36", plainTitle) : plainTitle;
|
|
909
|
-
const top = borderColor("╭") + titleText + borderColor("─".repeat(Math.max(0, innerWidth - plainTitle.length))) + borderColor("╮");
|
|
910
|
-
const bottom = borderColor("╰" + "─".repeat(innerWidth) + "╯");
|
|
911
|
-
return [
|
|
912
|
-
top,
|
|
913
|
-
...body.map((line) => {
|
|
914
|
-
const clipped = color ? truncateToWidth(line, contentWidth) : truncateDisplay(line, contentWidth);
|
|
915
|
-
return `${borderColor("│")} ${padAnsi(clipped, contentWidth)} ${borderColor("│")}`;
|
|
916
|
-
}),
|
|
917
|
-
bottom,
|
|
918
|
-
];
|
|
919
|
-
}
|
|
920
|
-
|
|
921
617
|
function dashboardLines(dashboard: Awaited<ReturnType<typeof buildDashboard>>, color = false): string[] {
|
|
922
618
|
const chips = arrayFromPayload(dashboard.chips);
|
|
923
619
|
const profile = dashboard.context.profile ?? "env/default";
|
|
@@ -1713,451 +1409,6 @@ async function buildForumEvidenceReply(client: QDashClient, params: ForumEvidenc
|
|
|
1713
1409
|
return { request, preview: content, parent, task, figures };
|
|
1714
1410
|
}
|
|
1715
1411
|
|
|
1716
|
-
function forumPostsFromPayload(payload: unknown): Record<string, unknown>[] {
|
|
1717
|
-
if (Array.isArray(payload)) return payload.filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object");
|
|
1718
|
-
if (payload && typeof payload === "object") {
|
|
1719
|
-
const object = payload as Record<string, unknown>;
|
|
1720
|
-
for (const key of ["posts", "items", "results", "data"]) {
|
|
1721
|
-
if (Array.isArray(object[key])) return object[key].filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object");
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
return [];
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
function forumPostId(post: Record<string, unknown>): string {
|
|
1728
|
-
return firstString(post, ["post_id", "id", "forum_post_id"]) ?? "unknown";
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
function forumPostTitle(post: Record<string, unknown>): string {
|
|
1732
|
-
return firstString(post, ["title", "subject", "summary"]) ?? firstString(post, ["content", "body", "text"])?.slice(0, 60) ?? "(untitled)";
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
function firstNumber(object: Record<string, unknown>, keys: string[]): number | undefined {
|
|
1736
|
-
for (const key of keys) {
|
|
1737
|
-
const value = object[key];
|
|
1738
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1739
|
-
}
|
|
1740
|
-
return undefined;
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
function stringList(value: unknown): string[] {
|
|
1744
|
-
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && item.length > 0) : [];
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
function forumPostLine(post: Record<string, unknown>): string {
|
|
1748
|
-
const number = firstNumber(post, ["number"]);
|
|
1749
|
-
const id = shortId(forumPostId(post), 10);
|
|
1750
|
-
const title = forumPostTitle(post);
|
|
1751
|
-
const status = firstString(post, ["status"]);
|
|
1752
|
-
const category = firstString(post, ["category"]);
|
|
1753
|
-
const targetType = firstString(post, ["target_type"]);
|
|
1754
|
-
const target = firstString(post, ["target_id", "chip_id", "task_id", "qid"]);
|
|
1755
|
-
const replies = firstNumber(post, ["reply_count"]);
|
|
1756
|
-
const assignee = firstString(post, ["assignee_username", "username"]);
|
|
1757
|
-
return [
|
|
1758
|
-
"•",
|
|
1759
|
-
number ? `#${number}` : id,
|
|
1760
|
-
title,
|
|
1761
|
-
category ? `[${category}]` : undefined,
|
|
1762
|
-
status ? `[${status}]` : undefined,
|
|
1763
|
-
target ? `(${targetType ? `${targetType}:` : ""}${target})` : undefined,
|
|
1764
|
-
replies && replies > 0 ? `↩${replies}` : undefined,
|
|
1765
|
-
assignee ? `@${assignee}` : undefined,
|
|
1766
|
-
].filter(Boolean).join(" ");
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
function inlineContentText(value: unknown): string {
|
|
1770
|
-
if (typeof value === "string") return value;
|
|
1771
|
-
if (!Array.isArray(value)) return "";
|
|
1772
|
-
return value.map((item) => {
|
|
1773
|
-
if (typeof item === "string") return item;
|
|
1774
|
-
if (!item || typeof item !== "object") return "";
|
|
1775
|
-
const object = item as Record<string, unknown>;
|
|
1776
|
-
if (typeof object.text === "string") return object.text;
|
|
1777
|
-
if (object.type === "link") return inlineContentText(object.content);
|
|
1778
|
-
return "";
|
|
1779
|
-
}).join("");
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
function forumBlockLines(blocks: unknown): string[] {
|
|
1783
|
-
if (!Array.isArray(blocks)) return [];
|
|
1784
|
-
const lines: string[] = [];
|
|
1785
|
-
for (const item of blocks) {
|
|
1786
|
-
if (!item || typeof item !== "object") continue;
|
|
1787
|
-
const block = item as Record<string, unknown>;
|
|
1788
|
-
const type = firstString(block, ["type"]) ?? "block";
|
|
1789
|
-
const props = block.props && typeof block.props === "object" ? block.props as Record<string, unknown> : {};
|
|
1790
|
-
const text = inlineContentText(block.content).trimEnd();
|
|
1791
|
-
if (type === "heading") {
|
|
1792
|
-
const level = firstNumber(props, ["level"]) ?? 2;
|
|
1793
|
-
lines.push(`${"#".repeat(Math.max(1, Math.min(6, level)))} ${text}`.trimEnd());
|
|
1794
|
-
} else if (type === "image") {
|
|
1795
|
-
const name = firstString(props, ["name", "caption"]) ?? "image";
|
|
1796
|
-
const url = firstString(props, ["url"]);
|
|
1797
|
-
lines.push(`🖼 ${name}${url ? ` ${url}` : ""}`);
|
|
1798
|
-
} else if (type === "bulletListItem") {
|
|
1799
|
-
lines.push(`- ${text}`);
|
|
1800
|
-
} else if (type === "numberedListItem") {
|
|
1801
|
-
lines.push(`1. ${text}`);
|
|
1802
|
-
} else if (text) {
|
|
1803
|
-
lines.push(...text.split("\n"));
|
|
1804
|
-
}
|
|
1805
|
-
const childLines = forumBlockLines(block.children);
|
|
1806
|
-
if (childLines.length > 0) lines.push(...childLines.map((line) => ` ${line}`));
|
|
1807
|
-
}
|
|
1808
|
-
return lines;
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
function forumContentLines(object: Record<string, unknown>): string[] {
|
|
1812
|
-
const blockLines = forumBlockLines(object.content_blocks);
|
|
1813
|
-
if (blockLines.length > 0) return blockLines;
|
|
1814
|
-
const content = firstString(object, ["content", "body", "text", "message", "description"]);
|
|
1815
|
-
return content ? content.split("\n") : ["(no content)"];
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
function forumListLines(payload: unknown, title = "QDash Forum", color = false): string[] {
|
|
1819
|
-
const posts = forumPostsFromPayload(payload);
|
|
1820
|
-
const total = payloadTotal(payload);
|
|
1821
|
-
const accent = (text: string) => color ? ansi("1;36", text) : text;
|
|
1822
|
-
const dim = (text: string) => color ? ansi("2", text) : text;
|
|
1823
|
-
return boxed(title, [
|
|
1824
|
-
`posts ${accent(`${posts.length}/${total}`)}`,
|
|
1825
|
-
"",
|
|
1826
|
-
...(posts.length > 0 ? posts.map((post) => ` ${forumPostLine(post)}`) : [` ${dim("none")}`]),
|
|
1827
|
-
], color);
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
|
-
function forumDetailBodyLines(post: unknown, color = false): string[] {
|
|
1831
|
-
const object = post && typeof post === "object" ? post as Record<string, unknown> : {};
|
|
1832
|
-
const accent = (text: string) => color ? ansi("1;36", text) : text;
|
|
1833
|
-
const muted = (text: string) => color ? ansi("90", text) : text;
|
|
1834
|
-
const labels = stringList(object.labels);
|
|
1835
|
-
const replyCount = firstNumber(object, ["reply_count"]);
|
|
1836
|
-
const meta = [
|
|
1837
|
-
firstString(object, ["category"]) ? `${muted("category")} ${firstString(object, ["category"])}` : undefined,
|
|
1838
|
-
firstString(object, ["status"]) ? `${muted("status")} ${firstString(object, ["status"])}` : undefined,
|
|
1839
|
-
firstString(object, ["target_type"]) || firstString(object, ["target_id"]) ? `${muted("target")} ${[firstString(object, ["target_type"]), firstString(object, ["target_id"])].filter(Boolean).join(":")}` : undefined,
|
|
1840
|
-
firstString(object, ["chip_id"]) ? `${muted("chip")} ${firstString(object, ["chip_id"])}` : undefined,
|
|
1841
|
-
replyCount !== undefined ? `${muted("replies")} ${replyCount}` : undefined,
|
|
1842
|
-
].filter((line): line is string => typeof line === "string");
|
|
1843
|
-
return [
|
|
1844
|
-
`${muted("id")} ${accent(forumPostId(object))}${firstNumber(object, ["number"]) ? ` ${muted("#")} ${firstNumber(object, ["number"])}` : ""}`,
|
|
1845
|
-
`${muted("title")} ${forumPostTitle(object)}`,
|
|
1846
|
-
`${muted("author")} ${firstString(object, ["username", "user_id"]) ?? "unknown"}${firstString(object, ["assignee_username"]) ? ` ${muted("assignee")} ${firstString(object, ["assignee_username"])}` : ""}`,
|
|
1847
|
-
meta.join(" "),
|
|
1848
|
-
labels.length > 0 ? `${muted("labels")} ${labels.map((label) => `#${label}`).join(" ")}` : undefined,
|
|
1849
|
-
firstString(object, ["created_at"]) ? `${muted("created")} ${firstString(object, ["created_at"])}${firstString(object, ["updated_at"]) ? ` ${muted("updated")} ${firstString(object, ["updated_at"])}` : ""}` : undefined,
|
|
1850
|
-
"",
|
|
1851
|
-
...forumContentLines(object).map((line) => ` ${line}`),
|
|
1852
|
-
].filter((line): line is string => typeof line === "string");
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
function forumDetailLines(post: unknown, title = "QDash Forum Post", color = false): string[] {
|
|
1856
|
-
return boxed(title, forumDetailBodyLines(post, color), color);
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
function wrapPlainLine(line: string, width: number): string[] {
|
|
1860
|
-
if (width <= 0 || visibleWidth(line) <= width) return [line];
|
|
1861
|
-
const output: string[] = [];
|
|
1862
|
-
let rest = line;
|
|
1863
|
-
while (visibleWidth(rest) > width) {
|
|
1864
|
-
let slice = "";
|
|
1865
|
-
for (const char of rest) {
|
|
1866
|
-
if (visibleWidth(slice + char) > width) break;
|
|
1867
|
-
slice += char;
|
|
1868
|
-
}
|
|
1869
|
-
output.push(slice);
|
|
1870
|
-
rest = rest.slice(slice.length);
|
|
1871
|
-
}
|
|
1872
|
-
if (rest.length > 0) output.push(rest);
|
|
1873
|
-
return output;
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
function boxLinesToWidth(title: string, body: string[], width: number, theme?: Theme): string[] {
|
|
1877
|
-
const contentWidth = Math.max(24, Math.min(100, width - 4));
|
|
1878
|
-
const border = (text: string) => theme ? theme.fg("borderMuted", text) : text;
|
|
1879
|
-
const titleText = ` ${title} `;
|
|
1880
|
-
const top = `${border("╭")}${theme ? theme.fg("accent", theme.bold(titleText)) : titleText}${border("─".repeat(Math.max(0, contentWidth + 2 - visibleWidth(titleText))))}${border("╮")}`;
|
|
1881
|
-
const bottom = border(`╰${"─".repeat(contentWidth + 2)}╯`);
|
|
1882
|
-
const rows = body.flatMap((line) => wrapPlainLine(line, contentWidth));
|
|
1883
|
-
return [
|
|
1884
|
-
top,
|
|
1885
|
-
...rows.map((line) => `${border("│")} ${padAnsi(truncateDisplay(line, contentWidth), contentWidth)} ${border("│")}`),
|
|
1886
|
-
bottom,
|
|
1887
|
-
];
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
function textComponent(lines: string[], _theme: Theme, wrap = false) {
|
|
1891
|
-
return {
|
|
1892
|
-
render(width: number) {
|
|
1893
|
-
if (!wrap) return lines.map((line) => truncateToWidth(line, width));
|
|
1894
|
-
return lines.flatMap((line) => wrapPlainLine(line, width));
|
|
1895
|
-
},
|
|
1896
|
-
invalidate() {},
|
|
1897
|
-
};
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
function forumDetailComponent(post: unknown, theme: Theme) {
|
|
1901
|
-
return {
|
|
1902
|
-
render(width: number) {
|
|
1903
|
-
return boxLinesToWidth("QDash Forum Post", forumDetailBodyLines(post), width, theme);
|
|
1904
|
-
},
|
|
1905
|
-
invalidate() {},
|
|
1906
|
-
};
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
type TimeseriesPoint = {
|
|
1910
|
-
series: string;
|
|
1911
|
-
value: number;
|
|
1912
|
-
at?: string;
|
|
1913
|
-
unit?: string;
|
|
1914
|
-
taskId?: string;
|
|
1915
|
-
executionId?: string;
|
|
1916
|
-
};
|
|
1917
|
-
|
|
1918
|
-
function timeseriesPoints(payload: unknown): TimeseriesPoint[] {
|
|
1919
|
-
const points: TimeseriesPoint[] = [];
|
|
1920
|
-
const addPoint = (series: string, item: unknown) => {
|
|
1921
|
-
if (!item || typeof item !== "object") return;
|
|
1922
|
-
const object = item as Record<string, unknown>;
|
|
1923
|
-
const value = object.value;
|
|
1924
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return;
|
|
1925
|
-
points.push({
|
|
1926
|
-
series,
|
|
1927
|
-
value,
|
|
1928
|
-
at: firstString(object, ["calibrated_at", "timestamp", "created_at", "start_at"]),
|
|
1929
|
-
unit: firstString(object, ["unit"]),
|
|
1930
|
-
taskId: firstString(object, ["task_id"]),
|
|
1931
|
-
executionId: firstString(object, ["execution_id"]),
|
|
1932
|
-
});
|
|
1933
|
-
};
|
|
1934
|
-
const visit = (value: unknown, series = "series") => {
|
|
1935
|
-
if (Array.isArray(value)) {
|
|
1936
|
-
for (const item of value) addPoint(series, item);
|
|
1937
|
-
return;
|
|
1938
|
-
}
|
|
1939
|
-
if (!value || typeof value !== "object") return;
|
|
1940
|
-
const object = value as Record<string, unknown>;
|
|
1941
|
-
if ("value" in object) {
|
|
1942
|
-
addPoint(series, object);
|
|
1943
|
-
return;
|
|
1944
|
-
}
|
|
1945
|
-
for (const [key, nested] of Object.entries(object)) visit(nested, key);
|
|
1946
|
-
};
|
|
1947
|
-
if (payload && typeof payload === "object" && "data" in payload) visit((payload as Record<string, unknown>).data);
|
|
1948
|
-
else visit(payload);
|
|
1949
|
-
return points.sort((a, b) => (a.at ?? "").localeCompare(b.at ?? ""));
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
function formatNumber(value: number): string {
|
|
1953
|
-
const abs = Math.abs(value);
|
|
1954
|
-
if (abs >= 1000 || (abs > 0 && abs < 0.001)) return value.toExponential(2);
|
|
1955
|
-
if (abs >= 100) return value.toFixed(1);
|
|
1956
|
-
if (abs >= 10) return value.toFixed(3);
|
|
1957
|
-
return value.toFixed(5).replace(/0+$/, "").replace(/\.$/, "");
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
function compactDate(value: string | undefined): string {
|
|
1961
|
-
if (!value) return "";
|
|
1962
|
-
const date = new Date(value.endsWith("Z") ? value : `${value}Z`);
|
|
1963
|
-
if (Number.isNaN(date.getTime())) return value.slice(0, 10);
|
|
1964
|
-
return `${String(date.getUTCMonth() + 1).padStart(2, "0")}/${String(date.getUTCDate()).padStart(2, "0")} ${String(date.getUTCHours()).padStart(2, "0")}:${String(date.getUTCMinutes()).padStart(2, "0")}`;
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
function samplePoints(points: TimeseriesPoint[], width: number): TimeseriesPoint[] {
|
|
1968
|
-
if (points.length <= width) return points;
|
|
1969
|
-
const sampled: TimeseriesPoint[] = [];
|
|
1970
|
-
for (let i = 0; i < width; i++) sampled.push(points[Math.round(i * (points.length - 1) / (width - 1))]);
|
|
1971
|
-
return sampled;
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
function plotSeriesLines(points: TimeseriesPoint[], options: { title: string; height?: number; width?: number; color?: boolean }): string[] {
|
|
1975
|
-
const height = Math.max(3, Math.min(20, options.height ?? 8));
|
|
1976
|
-
const plotWidth = Math.max(8, Math.min(100, options.width ?? 60));
|
|
1977
|
-
const accent = (text: string) => options.color ? ansi("1;36", text) : text;
|
|
1978
|
-
const muted = (text: string) => options.color ? ansi("90", text) : text;
|
|
1979
|
-
if (points.length === 0) return boxed(options.title, [muted("no numeric data")], options.color);
|
|
1980
|
-
|
|
1981
|
-
const sampled = samplePoints(points, plotWidth);
|
|
1982
|
-
const values = sampled.map((point) => point.value);
|
|
1983
|
-
const min = Math.min(...values);
|
|
1984
|
-
const max = Math.max(...values);
|
|
1985
|
-
const span = max - min || 1;
|
|
1986
|
-
const grid = Array.from({ length: height }, () => Array.from({ length: sampled.length }, () => " "));
|
|
1987
|
-
sampled.forEach((point, col) => {
|
|
1988
|
-
const row = height - 1 - Math.round((point.value - min) / span * (height - 1));
|
|
1989
|
-
grid[Math.max(0, Math.min(height - 1, row))][col] = "●";
|
|
1990
|
-
});
|
|
1991
|
-
const yLabelWidth = Math.max(formatNumber(max).length, formatNumber(min).length, 6);
|
|
1992
|
-
const rows = grid.map((row, index) => {
|
|
1993
|
-
const value = max - (span * index / (height - 1));
|
|
1994
|
-
return `${formatNumber(value).padStart(yLabelWidth)} ┤${row.join("")}`;
|
|
1995
|
-
});
|
|
1996
|
-
const unit = points.find((point) => point.unit)?.unit;
|
|
1997
|
-
const first = points[0];
|
|
1998
|
-
const last = points[points.length - 1];
|
|
1999
|
-
const body = [
|
|
2000
|
-
`${muted("series")} ${accent([...new Set(points.map((point) => point.series))].join(", "))}${unit ? ` ${muted("unit")} ${unit}` : ""}`,
|
|
2001
|
-
`${muted("count")} ${points.length} ${muted("min")} ${formatNumber(Math.min(...points.map((point) => point.value)))} ${muted("max")} ${formatNumber(Math.max(...points.map((point) => point.value)))} ${muted("last")} ${formatNumber(last.value)}`,
|
|
2002
|
-
`${muted("range")} ${compactDate(first.at)} → ${compactDate(last.at)}`,
|
|
2003
|
-
"",
|
|
2004
|
-
...rows,
|
|
2005
|
-
`${" ".repeat(yLabelWidth)} └${"─".repeat(sampled.length)}`,
|
|
2006
|
-
];
|
|
2007
|
-
return boxed(options.title, body, options.color);
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
|
-
function timeseriesPlotComponent(data: unknown, title: string, theme: Theme) {
|
|
2011
|
-
return {
|
|
2012
|
-
render(width: number) {
|
|
2013
|
-
const points = timeseriesPoints(data);
|
|
2014
|
-
return plotSeriesLines(points, { title, width: Math.max(8, width - 16) }).map((line) => truncateToWidth(line, width));
|
|
2015
|
-
},
|
|
2016
|
-
invalidate() {},
|
|
2017
|
-
};
|
|
2018
|
-
}
|
|
2019
|
-
|
|
2020
|
-
function mediaTypeForPath(path: string): string {
|
|
2021
|
-
const lower = path.toLowerCase();
|
|
2022
|
-
if (lower.endsWith(".png")) return "image/png";
|
|
2023
|
-
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
2024
|
-
if (lower.endsWith(".gif")) return "image/gif";
|
|
2025
|
-
if (lower.endsWith(".webp")) return "image/webp";
|
|
2026
|
-
if (lower.endsWith(".json")) return "application/json";
|
|
2027
|
-
return "application/octet-stream";
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
async function fetchFigureDetails(client: QDashClient, path: string): Promise<FigureDetails> {
|
|
2031
|
-
const file = await client.getExecutionFigure(path);
|
|
2032
|
-
const bytes = Buffer.from(file.data);
|
|
2033
|
-
const mediaType = file.mediaType || mediaTypeForPath(path);
|
|
2034
|
-
const details: FigureDetails = {
|
|
2035
|
-
tool: "qdash_get_figure",
|
|
2036
|
-
path,
|
|
2037
|
-
mediaType,
|
|
2038
|
-
sizeBytes: bytes.byteLength,
|
|
2039
|
-
};
|
|
2040
|
-
if (mediaType.startsWith("image/")) details.base64 = bytes.toString("base64");
|
|
2041
|
-
else details.text = bytes.toString("utf8");
|
|
2042
|
-
return details;
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
|
-
function figureResultText(details: FigureDetails): string {
|
|
2046
|
-
const lines = boxed("QDash Figure", [
|
|
2047
|
-
`path ${details.path}`,
|
|
2048
|
-
`type ${details.mediaType}`,
|
|
2049
|
-
`size ${details.sizeBytes} bytes`,
|
|
2050
|
-
...(details.taskId ? [`task ${details.taskId}`] : []),
|
|
2051
|
-
...(details.figurePaths?.length ? [`figures ${details.figurePaths.length}`] : []),
|
|
2052
|
-
...(details.jsonFigurePaths?.length ? [`json figures ${details.jsonFigurePaths.length}`] : []),
|
|
2053
|
-
...(details.text ? ["", ...details.text.split("\n").slice(0, 12).map((line) => ` ${line}`)] : []),
|
|
2054
|
-
]);
|
|
2055
|
-
return lines.join("\n");
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
|
-
function figureComponent(details: FigureDetails, theme: Theme) {
|
|
2059
|
-
if (details.base64 && details.mediaType.startsWith("image/")) {
|
|
2060
|
-
return new Image(details.base64, details.mediaType, { fallbackColor: (text: string) => theme.fg("dim", text) }, { maxWidthCells: 90, maxHeightCells: 30 });
|
|
2061
|
-
}
|
|
2062
|
-
return textComponent(figureResultText(details).split("\n"), theme);
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
type PlotlyTrace = Record<string, unknown>;
|
|
2066
|
-
|
|
2067
|
-
type FigureAnalysis = {
|
|
2068
|
-
path: string;
|
|
2069
|
-
taskId?: string;
|
|
2070
|
-
traceCount: number;
|
|
2071
|
-
summaries: string[];
|
|
2072
|
-
warnings: string[];
|
|
2073
|
-
};
|
|
2074
|
-
|
|
2075
|
-
function numericArray(value: unknown): number[] {
|
|
2076
|
-
return Array.isArray(value) ? value.filter((item): item is number => typeof item === "number" && Number.isFinite(item)) : [];
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
|
-
function numericMatrix(value: unknown): number[][] {
|
|
2080
|
-
return Array.isArray(value)
|
|
2081
|
-
? value.map((row) => numericArray(row)).filter((row) => row.length > 0)
|
|
2082
|
-
: [];
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
|
-
function matrixValue(matrix: number[][], row: number, column: number): number | undefined {
|
|
2086
|
-
return matrix[row]?.[column];
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
|
-
function compactStats(values: number[]): string {
|
|
2090
|
-
if (!values.length) return "no numeric values";
|
|
2091
|
-
const min = Math.min(...values);
|
|
2092
|
-
const max = Math.max(...values);
|
|
2093
|
-
const mean = values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
2094
|
-
return `n=${values.length} min=${formatNumber(min)} max=${formatNumber(max)} mean=${formatNumber(mean)}`;
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
function formatMaybeNumber(value: number | undefined): string {
|
|
2098
|
-
return typeof value === "number" && Number.isFinite(value) ? formatNumber(value) : "n/a";
|
|
2099
|
-
}
|
|
2100
|
-
|
|
2101
|
-
function analyzePlotlyFigure(path: string, figure: unknown, taskId?: string): FigureAnalysis {
|
|
2102
|
-
const object = figure && typeof figure === "object" ? figure as Record<string, unknown> : {};
|
|
2103
|
-
const traces = Array.isArray(object.data) ? object.data.filter((trace): trace is PlotlyTrace => Boolean(trace) && typeof trace === "object") : [];
|
|
2104
|
-
const summaries: string[] = [];
|
|
2105
|
-
const warnings: string[] = [];
|
|
2106
|
-
|
|
2107
|
-
traces.forEach((trace, index) => {
|
|
2108
|
-
const type = typeof trace.type === "string" ? trace.type : "unknown";
|
|
2109
|
-
const name = typeof trace.name === "string" ? trace.name : `trace ${index}`;
|
|
2110
|
-
if (type === "scatter" || Array.isArray(trace.y)) {
|
|
2111
|
-
const y = numericArray(trace.y);
|
|
2112
|
-
const x = numericArray(trace.x);
|
|
2113
|
-
const err = trace.error_y && typeof trace.error_y === "object" ? (trace.error_y as Record<string, unknown>).value : undefined;
|
|
2114
|
-
summaries.push(`${index}: scatter ${name} y(${compactStats(y)}) first=${formatMaybeNumber(y[0])} last=${formatMaybeNumber(y[y.length - 1])}${typeof err === "number" ? ` err=${formatNumber(err)}` : ""}`);
|
|
2115
|
-
if (y.length >= 5) {
|
|
2116
|
-
const tail = y.slice(Math.floor(y.length / 2));
|
|
2117
|
-
const tailSpan = Math.max(...tail) - Math.min(...tail);
|
|
2118
|
-
if (tailSpan > 0.4) warnings.push(`scatter ${name}: late repetitions still have large span ${formatNumber(tailSpan)}; repeated-pulse response may be unstable`);
|
|
2119
|
-
const firstStep = Math.abs((y[1] ?? y[0]) - y[0]);
|
|
2120
|
-
if (firstStep > 0.6 && Math.abs(y[y.length - 1]) > 0.1) warnings.push(`scatter ${name}: large first-step contrast but non-zero final offset ${formatNumber(y[y.length - 1])}`);
|
|
2121
|
-
}
|
|
2122
|
-
if (x.length && y.length && x.length !== y.length) warnings.push(`scatter ${name}: x/y length mismatch ${x.length}/${y.length}`);
|
|
2123
|
-
return;
|
|
2124
|
-
}
|
|
2125
|
-
|
|
2126
|
-
if (type === "heatmap" || Array.isArray(trace.z)) {
|
|
2127
|
-
const z = numericMatrix(trace.z);
|
|
2128
|
-
const flat = z.flat();
|
|
2129
|
-
summaries.push(`${index}: heatmap ${z.length}x${z[0]?.length ?? 0} z(${compactStats(flat)})`);
|
|
2130
|
-
if (z.length === 4 && z.every((row) => row.length === 4)) {
|
|
2131
|
-
const diag = [0, 1, 2, 3].map((i) => z[i][i]);
|
|
2132
|
-
const pop10 = diag[2];
|
|
2133
|
-
const coh0011Re = matrixValue(z, 0, 3);
|
|
2134
|
-
summaries.push(` 4x4 diag=[${diag.map(formatNumber).join(", ")}] z00,11=${formatMaybeNumber(coh0011Re)}`);
|
|
2135
|
-
if (pop10 > 0.15) warnings.push(`4x4 heatmap: |10> population/component is high (${formatNumber(pop10)}); suspect Bell/Zx angle, phase, or cancel error`);
|
|
2136
|
-
}
|
|
2137
|
-
return;
|
|
2138
|
-
}
|
|
2139
|
-
|
|
2140
|
-
summaries.push(`${index}: ${type}`);
|
|
2141
|
-
});
|
|
2142
|
-
|
|
2143
|
-
if (!traces.length) warnings.push("no Plotly data traces found");
|
|
2144
|
-
return { path, taskId, traceCount: traces.length, summaries, warnings };
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
|
-
function figureAnalysisText(analysis: FigureAnalysis): string {
|
|
2148
|
-
return boxed("QDash Figure Analysis", [
|
|
2149
|
-
`path ${analysis.path}`,
|
|
2150
|
-
...(analysis.taskId ? [`task ${analysis.taskId}`] : []),
|
|
2151
|
-
`traces ${analysis.traceCount}`,
|
|
2152
|
-
"",
|
|
2153
|
-
"Summaries",
|
|
2154
|
-
...analysis.summaries.map((line) => ` - ${line}`),
|
|
2155
|
-
"",
|
|
2156
|
-
"Warnings",
|
|
2157
|
-
...(analysis.warnings.length ? analysis.warnings.map((line) => ` - ${line}`) : [" - none"]),
|
|
2158
|
-
]).join("\n");
|
|
2159
|
-
}
|
|
2160
|
-
|
|
2161
1412
|
async function executeQuery(params: QDashQueryParams) {
|
|
2162
1413
|
params = applyQDashContext(params);
|
|
2163
1414
|
const client = await makeClient(params);
|
|
@@ -2215,25 +1466,7 @@ async function executeQuery(params: QDashQueryParams) {
|
|
|
2215
1466
|
}
|
|
2216
1467
|
|
|
2217
1468
|
export default function qdashExtension(pi: ExtensionAPI) {
|
|
2218
|
-
pi
|
|
2219
|
-
if (!WRITE_TOOL_NAMES.has(event.toolName)) return;
|
|
2220
|
-
const input = event.input as { confirmWrite?: boolean };
|
|
2221
|
-
if (input.confirmWrite === true) return;
|
|
2222
|
-
|
|
2223
|
-
if (!ctx.hasUI) {
|
|
2224
|
-
return {
|
|
2225
|
-
block: true,
|
|
2226
|
-
reason: `${event.toolName} is a QDash write operation and requires confirmWrite: true in non-interactive mode.`,
|
|
2227
|
-
};
|
|
2228
|
-
}
|
|
2229
|
-
|
|
2230
|
-
const ok = await ctx.ui.confirm(
|
|
2231
|
-
"Approve QDash write operation?",
|
|
2232
|
-
`${event.toolName} will create or modify data in QDash. Continue?`,
|
|
2233
|
-
);
|
|
2234
|
-
if (!ok) return { block: true, reason: "QDash write operation rejected by user" };
|
|
2235
|
-
input.confirmWrite = true;
|
|
2236
|
-
});
|
|
1469
|
+
installQDashWriteGate(pi);
|
|
2237
1470
|
|
|
2238
1471
|
pi.registerTool({
|
|
2239
1472
|
name: "qdash_config_info",
|
|
@@ -3281,6 +2514,105 @@ export default function qdashExtension(pi: ExtensionAPI) {
|
|
|
3281
2514
|
},
|
|
3282
2515
|
});
|
|
3283
2516
|
|
|
2517
|
+
pi.registerTool({
|
|
2518
|
+
name: "qdash_build_qcal_evidence",
|
|
2519
|
+
label: "QDash Build QCal Evidence",
|
|
2520
|
+
description: "Build a provider-neutral CalibrationEvidence bundle from a QDash task result for evaluation by pi-qcal.",
|
|
2521
|
+
promptSnippet: "Convert QDash calibration task results into pi-qcal CalibrationEvidence",
|
|
2522
|
+
promptGuidelines: [
|
|
2523
|
+
"Use qdash_build_qcal_evidence before qcal_evaluate_bundle when the user asks to evaluate QDash calibration data with pi-qcal.",
|
|
2524
|
+
"qdash_build_qcal_evidence is read-only and only converts QDash data into a generic evidence bundle; it does not evaluate, execute, commit, or apply calibration parameters.",
|
|
2525
|
+
"After qdash_build_qcal_evidence, pass details.evidence to qcal_evaluate_bundle and treat the result as advisory evidence.",
|
|
2526
|
+
],
|
|
2527
|
+
parameters: Type.Object({
|
|
2528
|
+
...connectionParams,
|
|
2529
|
+
taskId: Type.String(),
|
|
2530
|
+
includeFigures: Type.Optional(Type.Boolean({ description: "Embed task image figures as data URLs. Defaults to true." })),
|
|
2531
|
+
includeFigureAnalysis: Type.Optional(Type.Boolean({ description: "Analyze Plotly JSON figures and include summaries/warnings in notes. Defaults to true." })),
|
|
2532
|
+
maxFigures: Type.Optional(Type.Number({ description: "Maximum image figures to embed. Defaults to 1." })),
|
|
2533
|
+
includeRawTaskResult: Type.Optional(Type.Boolean({ description: "Include the full raw task result in resultJson. Defaults to false." })),
|
|
2534
|
+
}),
|
|
2535
|
+
async execute(_toolCallId, params: { profile?: string; configPath?: string; useEnv?: boolean; taskId: string; includeFigures?: boolean; includeFigureAnalysis?: boolean; maxFigures?: number; includeRawTaskResult?: boolean }) {
|
|
2536
|
+
const client = await makeClient(params);
|
|
2537
|
+
const task = await client.getTaskResult(params.taskId) as unknown as Record<string, unknown>;
|
|
2538
|
+
const figures: Array<Record<string, unknown>> = [];
|
|
2539
|
+
const notes: string[] = [];
|
|
2540
|
+
const figurePaths = Array.isArray(task.figure_path) ? task.figure_path.filter((item): item is string => typeof item === "string") : [];
|
|
2541
|
+
const jsonFigurePaths = Array.isArray(task.json_figure_path) ? task.json_figure_path.filter((item): item is string => typeof item === "string") : [];
|
|
2542
|
+
|
|
2543
|
+
if (params.includeFigureAnalysis !== false) {
|
|
2544
|
+
const limit = Math.min(jsonFigurePaths.length, params.maxFigures ?? 1);
|
|
2545
|
+
for (let index = 0; index < limit; index += 1) {
|
|
2546
|
+
try {
|
|
2547
|
+
const file: TaskResultFigureFile = await client.getTaskResultFigure(params.taskId, { index, preferJson: true });
|
|
2548
|
+
const figure = JSON.parse(Buffer.from(file.data).toString("utf8")) as unknown;
|
|
2549
|
+
const analysis = analyzePlotlyFigure(file.path ?? jsonFigurePaths[index] ?? "", figure, params.taskId);
|
|
2550
|
+
notes.push(`Figure JSON analysis #${index + 1}:`, ...analysis.summaries.map((line) => `- ${line}`));
|
|
2551
|
+
if (analysis.warnings.length) notes.push("Warnings:", ...analysis.warnings.map((line) => `- ${line}`));
|
|
2552
|
+
figures.push({ path: file.path ?? jsonFigurePaths[index], role: "plotly", data: figure });
|
|
2553
|
+
} catch (error) {
|
|
2554
|
+
notes.push(`Figure JSON analysis #${index + 1} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
if (params.includeFigures !== false) {
|
|
2560
|
+
const limit = Math.min(figurePaths.length, params.maxFigures ?? 1);
|
|
2561
|
+
for (let index = 0; index < limit; index += 1) {
|
|
2562
|
+
try {
|
|
2563
|
+
const file: TaskResultFigureFile = await client.getTaskResultFigure(params.taskId, { index, preferJson: false });
|
|
2564
|
+
const bytes = Buffer.from(file.data);
|
|
2565
|
+
const mediaType = file.mediaType || mediaTypeForPath(file.path ?? figurePaths[index] ?? "");
|
|
2566
|
+
if (mediaType.startsWith("image/")) {
|
|
2567
|
+
figures.push({ path: file.path ?? figurePaths[index], url: `data:${mediaType};base64,${bytes.toString("base64")}`, mimeType: mediaType, role: "plot" });
|
|
2568
|
+
}
|
|
2569
|
+
} catch (error) {
|
|
2570
|
+
notes.push(`Figure image #${index + 1} embedding failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
const couplingId = firstStringField(task, ["coupling_id", "couplingId", "qid"]);
|
|
2576
|
+
const qid = firstStringField(task, ["qid"]);
|
|
2577
|
+
const evidence: CalibrationEvidence = {
|
|
2578
|
+
id: params.taskId,
|
|
2579
|
+
source: "external",
|
|
2580
|
+
target: {
|
|
2581
|
+
kind: couplingId && couplingId.includes("-") ? "coupling" : qid ? "qubit" : "system",
|
|
2582
|
+
id: couplingId ?? qid,
|
|
2583
|
+
couplingId: couplingId && couplingId.includes("-") ? couplingId : undefined,
|
|
2584
|
+
qids: qid && !qid.includes("-") ? [qid] : undefined,
|
|
2585
|
+
chipId: firstStringField(task, ["chip_id", "chipId"]),
|
|
2586
|
+
},
|
|
2587
|
+
task: {
|
|
2588
|
+
name: firstStringField(task, ["task_name", "taskName", "name"]),
|
|
2589
|
+
objective: `Evaluate QDash calibration task ${firstStringField(task, ["task_name", "taskName", "name"]) ?? params.taskId}`,
|
|
2590
|
+
backend: firstStringField(task, ["backend", "chip_id", "chipId"]),
|
|
2591
|
+
startedAt: firstStringField(task, ["start_at", "startAt"]),
|
|
2592
|
+
completedAt: firstStringField(task, ["end_at", "endAt"]),
|
|
2593
|
+
status: firstStringField(task, ["status"]),
|
|
2594
|
+
},
|
|
2595
|
+
metrics: parameterValueRecord(task.output_parameters),
|
|
2596
|
+
parameters: { ...parameterValueRecord(task.input_parameters), ...parameterValueRecord(task.run_parameters) },
|
|
2597
|
+
figures: figures.length ? figures : undefined,
|
|
2598
|
+
notes: [
|
|
2599
|
+
typeof task.message === "string" ? `Task message: ${task.message}` : undefined,
|
|
2600
|
+
notes.length ? notes.join("\n") : undefined,
|
|
2601
|
+
].filter(Boolean).join("\n\n") || undefined,
|
|
2602
|
+
resultJson: params.includeRawTaskResult ? task : {
|
|
2603
|
+
task_id: task.task_id,
|
|
2604
|
+
execution_id: task.execution_id,
|
|
2605
|
+
flow_name: task.flow_name,
|
|
2606
|
+
figure_path: figurePaths,
|
|
2607
|
+
json_figure_path: jsonFigurePaths,
|
|
2608
|
+
links: task._links,
|
|
2609
|
+
},
|
|
2610
|
+
};
|
|
2611
|
+
|
|
2612
|
+
return toTextToolResult(qcalEvidenceText(evidence), { evidence }, { tool: "qdash_build_qcal_evidence", taskId: params.taskId, evidence });
|
|
2613
|
+
},
|
|
2614
|
+
});
|
|
2615
|
+
|
|
3284
2616
|
registerQueryTool({
|
|
3285
2617
|
name: "qdash_get_provenance_stats",
|
|
3286
2618
|
label: "QDash Provenance Stats",
|