feat: add lda report workflow demo panel

This commit is contained in:
lda
2026-07-03 00:20:41 +07:00 Verified
parent 86ccbe35cf
commit f5a0d2d9e2
18 changed files with 1166 additions and 12 deletions
+2
View File
@@ -21,6 +21,8 @@ export {
WorkflowDeploymentsValidate,
WorkflowRunsList,
WorkflowRunsInspect,
WorkflowRunsStart,
WorkflowRunsResume,
WorkflowRunsTrace,
WorkflowRpcs,
ArtifactRefSchema,
+74 -1
View File
@@ -16,6 +16,8 @@ import {
WorkflowRunsListResultSchema,
WorkflowRunsInspectPayloadSchema,
WorkflowRunsInspectResultSchema,
WorkflowRunsStartPayloadSchema,
WorkflowRunsResumePayloadSchema,
WorkflowRunsTracePayloadSchema,
WorkflowRunsTraceResultSchema,
} from "./rpcs.js";
@@ -24,7 +26,7 @@ export type OperationMeta = {
readonly method: string;
readonly label: string;
readonly explanation: string;
readonly idempotency: "read";
readonly idempotency: "read" | "write";
readonly equivalentCli: (params: unknown) => string;
readonly interpret: (result: unknown) => unknown;
};
@@ -68,6 +70,37 @@ const interpretNextActions = (nextActions: {
warnings: nextActions.warnings,
});
/** Adapts a snake_case run detail from the server into camelCase for the browser. */
const interpretRunDetail = (decoded: {
readonly run_id: string;
readonly deployment_id: string;
readonly artifact_id: string;
readonly artifact_version: number;
readonly status: string;
readonly resume_readiness: string;
readonly interrupt: unknown;
readonly outcome: string | null;
readonly error: string | null;
readonly output: Record<string, unknown> | null;
readonly diagnostics: ReadonlyArray<unknown>;
readonly trace_count: number;
readonly next_actions: Parameters<typeof interpretNextActions>[0];
}) => ({
runId: decoded.run_id,
deploymentId: decoded.deployment_id,
artifactId: decoded.artifact_id,
artifactVersion: decoded.artifact_version,
status: decoded.status,
resumeReadiness: decoded.resume_readiness,
interrupt: decoded.interrupt,
outcome: decoded.outcome,
error: decoded.error,
output: decoded.output,
diagnostics: decoded.diagnostics,
traceCount: decoded.trace_count,
nextActions: interpretNextActions(decoded.next_actions),
});
const operationEntries: ReadonlyArray<OperationMeta> = [
{
method: "workflow.health",
@@ -324,6 +357,46 @@ const operationEntries: ReadonlyArray<OperationMeta> = [
};
},
},
{
method: "workflow.runs.start",
label: "Start run",
explanation: "Start a workflow deployment run",
idempotency: "write",
equivalentCli: (params) => {
const p = Schema.decodeUnknownSync(WorkflowRunsStartPayloadSchema)(
params,
{ onExcessProperty: "error" },
);
return `uv run wf run start ${p.deployment_id} --input '<json>'`;
},
interpret: (result) => {
const decoded = Schema.decodeUnknownSync(WorkflowRunsInspectResultSchema)(
result,
{ onExcessProperty: "ignore" },
);
return interpretRunDetail(decoded);
},
},
{
method: "workflow.runs.resume",
label: "Resume run",
explanation: "Resume an interrupted workflow run",
idempotency: "write",
equivalentCli: (params) => {
const p = Schema.decodeUnknownSync(WorkflowRunsResumePayloadSchema)(
params,
{ onExcessProperty: "error" },
);
return `uv run wf run resume ${p.run_id} --payload '<json>'`;
},
interpret: (result) => {
const decoded = Schema.decodeUnknownSync(WorkflowRunsInspectResultSchema)(
result,
{ onExcessProperty: "ignore" },
);
return interpretRunDetail(decoded);
},
},
{
method: "workflow.runs.trace",
label: "Read run trace",
+30
View File
@@ -242,6 +242,9 @@ const RunInterruptSchema = Schema.Struct({
kind: Schema.String,
payload: JsonObjectSchema,
outcomes: Schema.Array(Schema.String),
request_schema: Schema.optional(JsonObjectSchema),
resume_schema: Schema.optional(JsonObjectSchema),
typed: Schema.optional(Schema.Boolean),
});
const RunNextActionsSchema = Schema.Struct({
@@ -275,6 +278,31 @@ export const WorkflowRunsInspect = Rpc.make("workflow.runs.inspect", {
error: Schema.Never,
});
export const WorkflowRunsStartPayloadSchema = Schema.Struct({
deployment_id: Schema.String,
workflow_input: JsonObjectSchema,
trace_range: Schema.optional(Schema.NullOr(TraceRangeSchema)),
});
export const WorkflowRunsResumePayloadSchema = Schema.Struct({
run_id: Schema.String,
resume_payload: JsonObjectSchema,
resume_outcome: Schema.optional(Schema.String),
trace_range: Schema.optional(Schema.NullOr(TraceRangeSchema)),
});
export const WorkflowRunsStart = Rpc.make("workflow.runs.start", {
payload: WorkflowRunsStartPayloadSchema,
success: WorkflowRunsInspectResultSchema,
error: Schema.Never,
});
export const WorkflowRunsResume = Rpc.make("workflow.runs.resume", {
payload: WorkflowRunsResumePayloadSchema,
success: WorkflowRunsInspectResultSchema,
error: Schema.Never,
});
export const WorkflowRunsTracePayloadSchema = Schema.Struct({
run_id: Schema.String,
trace_range: TraceRangeSchema,
@@ -314,5 +342,7 @@ export const WorkflowRpcs = RpcGroup.make(
WorkflowDeploymentsValidate,
WorkflowRunsList,
WorkflowRunsInspect,
WorkflowRunsStart,
WorkflowRunsResume,
WorkflowRunsTrace,
);
+145
View File
@@ -210,6 +210,115 @@ const lifecycleCases = [
},
},
},
{
operation: "workflow.runs.start" as const,
params: {
deployment_id: "lda_report_case_study.default",
workflow_input: {
selected_documents: [
"project-brief.md",
"architecture-notes.md",
"evaluation-findings.md",
"risk-register.md",
"roadmap.md",
],
board_path: "issue-board.json",
},
trace_range: { start: 0, limit: 50 },
},
result: {
run_id: "run_demo",
deployment_id: "lda_report_case_study.default",
artifact_id: "lda_report_case_study",
artifact_version: 1,
status: "interrupted",
resume_readiness: "ready",
interrupt: {
kind: "issue_review",
payload: {
report_markdown: "# lda.chat Thesis And Project Readiness Report",
proposed_issues: [
{
id: "demo-issue-1",
title: "Prepare demo script",
body: "Write the defense walkthrough.",
severity: "medium",
},
],
},
outcomes: ["submitted", "cancelled"],
request_schema: {
type: "object",
required: ["report_markdown", "proposed_issues"],
},
resume_schema: {
type: "object",
required: ["approved", "selected_issue_ids"],
},
typed: true,
},
outcome: null,
error: null,
output: null,
diagnostics: [],
trace_count: 1,
next_actions: {
can_continue: true,
can_save_now: null,
recommended_next_tool: "wf.workflow.resume_run",
reason: "run is interrupted",
patch_examples: [],
warnings: [],
},
},
},
{
operation: "workflow.runs.resume" as const,
params: {
run_id: "run_demo",
resume_payload: {
approved: true,
selected_issue_ids: ["demo-issue-1"],
comment: "Create selected issues.",
},
resume_outcome: "submitted",
trace_range: { start: 0, limit: 50 },
},
result: {
run_id: "run_demo",
deployment_id: "lda_report_case_study.default",
artifact_id: "lda_report_case_study",
artifact_version: 1,
status: "completed",
resume_readiness: "not_applicable",
interrupt: null,
outcome: "completed",
error: null,
output: {
approved: true,
markdown: "# lda.chat Thesis And Project Readiness Report",
created_issues: [
{
id: "ISSUE-001",
title: "Prepare demo script",
url: "local://issues/ISSUE-001",
},
],
selected_issue_ids: ["demo-issue-1"],
comment: "Create selected issues.",
},
diagnostics: [],
trace_count: 4,
next_actions: {
can_continue: false,
can_save_now: null,
recommended_next_tool: null,
reason: "Run completed.",
patch_examples: [],
warnings: [],
},
},
},
{
operation: "workflow.runs.trace" as const,
params: { run_id: "run_1", trace_range: { start: 0, limit: 50 } },
@@ -472,4 +581,40 @@ describe("lifecycle operations", () => {
});
expect(exchange.interpreted).not.toHaveProperty("trace");
});
it("interprets typed interrupt contracts from run start", async () => {
const startCase = lifecycleCases.find(
(testCase) => testCase.operation === "workflow.runs.start",
);
expect(startCase).toBeDefined();
if (!startCase) return;
const fetch: typeof globalThis.fetch = async (input, init) => {
const request = await requestBody(input, init);
return jsonResponse({
jsonrpc: "2.0",
id: request.id,
result: startCase.result,
});
};
const exchange = await runOperation(
{ fetch },
startCase.operation as "workflow.health" | "workflow.sources.list",
startCase.params,
);
expect(exchange.interpreted).toMatchObject({
runId: "run_demo",
status: "interrupted",
interrupt: {
kind: "issue_review",
typed: true,
outcomes: ["submitted", "cancelled"],
},
nextActions: {
canContinue: true,
},
});
});
});
+20
View File
@@ -34,6 +34,8 @@ import {
WorkflowDeploymentsValidatePayloadSchema,
WorkflowRunsListPayloadSchema,
WorkflowRunsInspectPayloadSchema,
WorkflowRunsStartPayloadSchema,
WorkflowRunsResumePayloadSchema,
WorkflowRunsTracePayloadSchema,
} from "./rpcs.js";
import { normalizeLoopbackTarget } from "./target-policy.js";
@@ -51,6 +53,8 @@ export type OperationName =
| "workflow.deployments.validate"
| "workflow.runs.list"
| "workflow.runs.inspect"
| "workflow.runs.start"
| "workflow.runs.resume"
| "workflow.runs.trace";
export interface WorkflowRpcOptions {
@@ -89,6 +93,8 @@ const isOperationName = (value: string): value is OperationName =>
value === "workflow.deployments.validate" ||
value === "workflow.runs.list" ||
value === "workflow.runs.inspect" ||
value === "workflow.runs.start" ||
value === "workflow.runs.resume" ||
value === "workflow.runs.trace";
const toExchange = (evidence: EvidenceRecord | null): RpcExchangeEvidence => ({
@@ -338,6 +344,20 @@ const executeImpl =
);
return yield* client.workflow["runs.inspect"](payload);
}
case "workflow.runs.start": {
const payload = yield* decodeParams(
WorkflowRunsStartPayloadSchema,
params,
);
return yield* client.workflow["runs.start"](payload);
}
case "workflow.runs.resume": {
const payload = yield* decodeParams(
WorkflowRunsResumePayloadSchema,
params,
);
return yield* client.workflow["runs.resume"](payload);
}
case "workflow.runs.trace": {
const payload = yield* decodeParams(
WorkflowRunsTracePayloadSchema,