agentweaver 0.1.18 → 0.1.19

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 CHANGED
@@ -146,6 +146,14 @@ The Web UI serves the operator console from the same local process, including `/
146
146
 
147
147
  Web UI state is process-local: it exists only while the AgentWeaver process is running and is not shared with other AgentWeaver processes. The Web UI is intended to match the interactive operator workflow for flow selection, launch confirmation, routing and user-input forms, progress and logs, and interrupt handling.
148
148
 
149
+ ### Artifact Explorer
150
+
151
+ After a Web UI workflow completes, and also after a failed run when artifacts were written before failure, the Web UI offers the Artifact Explorer. It reads artifacts from the active AgentWeaver scope only, optionally filtered to the run id published by the completed workflow. The browser requests artifact content through safe artifact identifiers resolved by the active catalog and registry; it does not accept arbitrary filesystem paths.
152
+
153
+ The MVP explorer previews Markdown, JSON, plain text, and diff artifacts. Diffs are rendered as text. Binary and unknown artifacts are listed with metadata and safe raw/download actions, but their inline preview is a placeholder. Large previews are bounded and marked as truncated with loaded byte and total size metadata; raw and download links can still serve the full artifact bytes with no-store cache headers and safe content types.
154
+
155
+ When Web UI credentials are configured, the same HTTP Basic auth protection applies to `/`, `/static/*`, `/__agentweaver/ws`, `/__agentweaver/exit`, and the artifact list, preview, raw, and download API routes. The MVP explorer does not support artifact editing, comments, run comparison, image previews, specialized viewers, live updates, or full-text search.
156
+
149
157
  ## Installation
150
158
 
151
159
  Local development:
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ import { requestUserInputInTerminal } from "./user-input.js";
38
38
  import { runDoctorCommand } from "./doctor/index.js";
39
39
  import { requestJiraContext, resolveProjectScope, } from "./scope.js";
40
40
  const COMMANDS = [
41
+ "auto",
41
42
  "auto-golang",
42
43
  "auto-common-guided",
43
44
  "auto-common",
@@ -174,6 +175,8 @@ function usage() {
174
175
  agentweaver review-loop [--dry] [--verbose] [--prompt <text>] [--scope <name>] [--blocking-severities <list>] [<jira-browse-url|jira-issue-key>]
175
176
  agentweaver run-go-tests-loop [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
176
177
  agentweaver run-go-linter-loop [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
178
+ agentweaver auto [--dry] [--verbose] [--prompt <text>] [--md-lang <en|ru>] <jira-browse-url|jira-issue-key>
179
+ agentweaver auto --help-phases
177
180
  agentweaver auto-golang [--dry] [--verbose] [--prompt <text>] [<jira-browse-url|jira-issue-key>]
178
181
  agentweaver auto-golang [--dry] [--verbose] [--prompt <text>] --from <phase> [<jira-browse-url|jira-issue-key>]
179
182
  agentweaver auto-golang --help-phases
@@ -1481,11 +1484,12 @@ async function parseCliArgs(argv) {
1481
1484
  writeStderrSync(`${usage()}\n`);
1482
1485
  process.exit(1);
1483
1486
  }
1484
- const command = argv[0];
1485
- if (!COMMANDS.includes(command)) {
1487
+ const rawCommand = argv[0];
1488
+ if (!COMMANDS.includes(rawCommand)) {
1486
1489
  writeStderrSync(`${usage()}\n`);
1487
1490
  process.exit(1);
1488
1491
  }
1492
+ const command = rawCommand === "auto" ? "auto-common" : rawCommand;
1489
1493
  let dry = false;
1490
1494
  let verbose = false;
1491
1495
  let prompt;
@@ -522,6 +522,79 @@ export class InteractiveSessionController {
522
522
  setScrollOffset(panel, offset) {
523
523
  this.applyScrollOffset(panel, offset, this.panelMaxScroll(panel));
524
524
  }
525
+ getCurrentFlowExecutionState() {
526
+ return this.state.flowState.executionState;
527
+ }
528
+ hasActiveInput() {
529
+ return this.confirmSession !== null || this.activeFormSession !== null;
530
+ }
531
+ setArtifactExplorerAvailability(input) {
532
+ const count = input.artifactCount;
533
+ const hasCount = typeof count === "number";
534
+ const failed = input.status === "failed";
535
+ const label = input.label
536
+ ?? (failed
537
+ ? hasCount && count > 0 ? "Run failed; artifacts available" : "Run failed"
538
+ : hasCount && count === 0 ? "Run completed; no artifacts found" : "Artifacts ready");
539
+ const message = input.message
540
+ ?? (failed
541
+ ? hasCount && count > 0
542
+ ? "The workflow failed, but artifacts are available for review."
543
+ : "The workflow failed. The explorer can check for any artifacts written before failure."
544
+ : hasCount && count === 0
545
+ ? "The workflow completed, but no artifacts were found for this run yet."
546
+ : "The workflow completed and artifacts are available for review.");
547
+ this.state.artifactExplorer = {
548
+ available: true,
549
+ open: Boolean(input.open) && !this.hasActiveInput(),
550
+ scopeKey: input.scopeKey,
551
+ runId: input.runId ?? null,
552
+ ...(input.runIds && input.runIds.length > 1 ? { runIds: input.runIds } : {}),
553
+ status: input.status,
554
+ label,
555
+ ...(hasCount ? { artifactCount: count } : {}),
556
+ message,
557
+ };
558
+ this.emitChange();
559
+ }
560
+ setArtifactExplorerUnavailable(message = "Artifacts are available after a Web UI workflow run completes.") {
561
+ if (!this.state.artifactExplorer.available && !this.state.artifactExplorer.open) {
562
+ return;
563
+ }
564
+ this.state.artifactExplorer = {
565
+ available: false,
566
+ open: false,
567
+ scopeKey: null,
568
+ runId: null,
569
+ status: "unavailable",
570
+ label: "Artifact Explorer",
571
+ message,
572
+ };
573
+ this.emitChange();
574
+ }
575
+ closeArtifactExplorer() {
576
+ if (!this.state.artifactExplorer.open) {
577
+ return;
578
+ }
579
+ this.state.artifactExplorer = {
580
+ ...this.state.artifactExplorer,
581
+ open: false,
582
+ };
583
+ this.emitChange();
584
+ }
585
+ openArtifactExplorer() {
586
+ if (!this.state.artifactExplorer.available || this.hasActiveInput()) {
587
+ return;
588
+ }
589
+ if (this.state.artifactExplorer.open) {
590
+ return;
591
+ }
592
+ this.state.artifactExplorer = {
593
+ ...this.state.artifactExplorer,
594
+ open: true,
595
+ };
596
+ this.emitChange();
597
+ }
525
598
  getViewModel(layout) {
526
599
  const selectedItem = this.selectedFlowTreeItem();
527
600
  const activeFlowId = this.activeFlowId();
@@ -566,6 +639,7 @@ export class InteractiveSessionController {
566
639
  confirmText: this.renderConfirmText(),
567
640
  confirmation: this.renderConfirmationView(),
568
641
  form: this.renderFormView(layout),
642
+ artifactExplorer: { ...this.state.artifactExplorer },
569
643
  };
570
644
  }
571
645
  emitChange(event = { type: "render" }) {
@@ -31,5 +31,14 @@ export function createInitialInteractiveState(options) {
31
31
  summaryScrollOffset: 0,
32
32
  logScrollOffset: 0,
33
33
  helpScrollOffset: 0,
34
+ artifactExplorer: {
35
+ available: false,
36
+ open: false,
37
+ scopeKey: null,
38
+ runId: null,
39
+ status: "unavailable",
40
+ label: "Artifact Explorer",
41
+ message: "Artifacts are available after a Web UI workflow run completes.",
42
+ },
34
43
  };
35
44
  }
@@ -1,8 +1,10 @@
1
1
  import process from "node:process";
2
2
  import { writeSync } from "node:fs";
3
3
  import { FlowInterruptedError } from "../../errors.js";
4
+ import { listArtifactCatalog } from "../../runtime/artifact-catalog.js";
5
+ import { createArtifactRegistry } from "../../runtime/artifact-registry.js";
4
6
  import { InteractiveSessionController } from "../controller.js";
5
- import { startWebServer } from "./server.js";
7
+ import { startWebServer, } from "./server.js";
6
8
  function actionId(action) {
7
9
  return "actionId" in action ? action.actionId : undefined;
8
10
  }
@@ -12,6 +14,17 @@ export function createWebInteractiveSession(options, webOptions = {}) {
12
14
  let unsubscribe = null;
13
15
  let mounted = false;
14
16
  let shuttingDown = false;
17
+ let activeScopeKey = options.scopeKey;
18
+ let artifactRestoreGeneration = 0;
19
+ const artifactCatalogProvider = webOptions.getArtifactCatalog ?? ((input) => {
20
+ const explorerScopeKey = controller.getViewModel().artifactExplorer.scopeKey;
21
+ const requestedScopeKey = input?.scopeKey;
22
+ const scopeKey = requestedScopeKey && requestedScopeKey === explorerScopeKey ? requestedScopeKey : activeScopeKey;
23
+ return listArtifactCatalog({
24
+ scopeKey,
25
+ artifactRegistry: createArtifactRegistry(),
26
+ });
27
+ });
15
28
  function snapshot() {
16
29
  return { type: "snapshot", viewModel: controller.getViewModel() };
17
30
  }
@@ -24,7 +37,117 @@ export function createWebInteractiveSession(options, webOptions = {}) {
24
37
  server?.broadcast(event);
25
38
  }
26
39
  }
40
+ function uniqueStrings(values) {
41
+ return values.filter((value, index, allValues) => (typeof value === "string" && value.length > 0 && allValues.indexOf(value) === index));
42
+ }
43
+ function collectPublishedArtifactRunIds(executionState) {
44
+ const runIds = [];
45
+ for (const phase of executionState?.phases ?? []) {
46
+ for (const step of phase.steps) {
47
+ for (const artifact of step.publishedArtifacts ?? []) {
48
+ const runId = artifact.manifest?.run_id;
49
+ if (runId) {
50
+ runIds.push(runId);
51
+ }
52
+ }
53
+ }
54
+ }
55
+ return uniqueStrings(runIds);
56
+ }
57
+ function candidateRunIds() {
58
+ const executionState = controller.getCurrentFlowExecutionState();
59
+ return uniqueStrings([
60
+ executionState?.runId ?? null,
61
+ executionState?.publicationRunId ?? null,
62
+ ...collectPublishedArtifactRunIds(executionState),
63
+ ]);
64
+ }
65
+ async function resolveArtifactExplorerRunMetadata(scopeKey) {
66
+ const candidates = candidateRunIds();
67
+ const preferredRunId = candidates[0] ?? null;
68
+ try {
69
+ const catalog = await artifactCatalogProvider({
70
+ scopeKey,
71
+ ...(candidates.length === 1 && preferredRunId ? { runId: preferredRunId, runIds: candidates } : {}),
72
+ ...(candidates.length > 1 ? { runIds: candidates } : {}),
73
+ });
74
+ if (!catalog || catalog.scopeKey !== scopeKey) {
75
+ return { runId: preferredRunId };
76
+ }
77
+ if (candidates.length === 0) {
78
+ return {
79
+ runId: null,
80
+ artifactCount: catalog.items.filter((item) => item.scopeKey === scopeKey && item.kind === "markdown").length,
81
+ };
82
+ }
83
+ const matchingRunIds = candidates.filter((candidate) => (catalog.items.some((item) => item.scopeKey === scopeKey && item.runId === candidate && item.kind === "markdown")));
84
+ if (matchingRunIds.length > 0) {
85
+ const matchingRunIdSet = new Set(matchingRunIds);
86
+ return {
87
+ runId: matchingRunIds[0] ?? preferredRunId,
88
+ ...(matchingRunIds.length > 1 ? { runIds: matchingRunIds } : {}),
89
+ artifactCount: catalog.items.filter((item) => (item.scopeKey === scopeKey
90
+ && item.kind === "markdown"
91
+ && item.runId !== null
92
+ && matchingRunIdSet.has(item.runId))).length,
93
+ };
94
+ }
95
+ return {
96
+ runId: preferredRunId,
97
+ artifactCount: catalog.items.filter((item) => item.scopeKey === scopeKey && item.kind === "markdown" && item.runId === preferredRunId).length,
98
+ };
99
+ }
100
+ catch {
101
+ return { runId: preferredRunId };
102
+ }
103
+ }
104
+ async function markArtifactExplorerForCompletedRun(status) {
105
+ artifactRestoreGeneration += 1;
106
+ const scopeKey = activeScopeKey;
107
+ const { runId, runIds, artifactCount } = await resolveArtifactExplorerRunMetadata(scopeKey);
108
+ controller.setArtifactExplorerAvailability({
109
+ scopeKey,
110
+ runId,
111
+ ...(runIds ? { runIds } : {}),
112
+ status,
113
+ ...(artifactCount !== undefined ? { artifactCount } : {}),
114
+ open: !controller.hasActiveInput(),
115
+ });
116
+ }
117
+ async function restoreArtifactExplorerFromScope(scopeKey) {
118
+ const generation = ++artifactRestoreGeneration;
119
+ try {
120
+ const catalog = await artifactCatalogProvider({ scopeKey });
121
+ if (generation !== artifactRestoreGeneration || shuttingDown || activeScopeKey !== scopeKey) {
122
+ return;
123
+ }
124
+ if (!catalog || catalog.scopeKey !== scopeKey) {
125
+ controller.setArtifactExplorerUnavailable();
126
+ return;
127
+ }
128
+ const artifactCount = catalog.items.filter((item) => item.scopeKey === scopeKey && item.kind === "markdown").length;
129
+ if (artifactCount === 0) {
130
+ controller.setArtifactExplorerUnavailable("No markdown artifacts were found for the current scope.");
131
+ return;
132
+ }
133
+ controller.setArtifactExplorerAvailability({
134
+ scopeKey,
135
+ runId: null,
136
+ status: "completed",
137
+ artifactCount,
138
+ open: false,
139
+ label: "Artifacts available",
140
+ message: "Markdown artifacts from this scope are available for review.",
141
+ });
142
+ }
143
+ catch {
144
+ if (generation === artifactRestoreGeneration && !shuttingDown && activeScopeKey === scopeKey) {
145
+ controller.setArtifactExplorerUnavailable("Artifact Explorer could not inspect the current scope.");
146
+ }
147
+ }
148
+ }
27
149
  async function dispatch(action, client) {
150
+ let acceptedRunConfirmation = false;
28
151
  try {
29
152
  if (action.type === "flow.select") {
30
153
  if (action.key) {
@@ -48,10 +171,16 @@ export function createWebInteractiveSession(options, webOptions = {}) {
48
171
  return;
49
172
  }
50
173
  if (action.type === "confirm.accept") {
174
+ const confirmation = controller.getViewModel().confirmation;
175
+ const acceptedAction = action.action ?? confirmation?.selectedAction;
176
+ acceptedRunConfirmation = confirmation?.kind === "run" && acceptedAction !== "cancel";
51
177
  if (action.action) {
52
178
  controller.selectConfirmAction(action.action);
53
179
  }
54
180
  await controller.acceptConfirmation();
181
+ if (acceptedRunConfirmation) {
182
+ await markArtifactExplorerForCompletedRun("completed");
183
+ }
55
184
  return;
56
185
  }
57
186
  if (action.type === "confirm.cancel") {
@@ -86,6 +215,14 @@ export function createWebInteractiveSession(options, webOptions = {}) {
86
215
  controller.clearLog();
87
216
  return;
88
217
  }
218
+ if (action.type === "artifactExplorer.open") {
219
+ controller.openArtifactExplorer();
220
+ return;
221
+ }
222
+ if (action.type === "artifactExplorer.close") {
223
+ controller.closeArtifactExplorer();
224
+ return;
225
+ }
89
226
  if (action.type === "help.toggle") {
90
227
  controller.showHelp(action.visible ?? !controller.getViewModel().helpVisible);
91
228
  return;
@@ -93,6 +230,9 @@ export function createWebInteractiveSession(options, webOptions = {}) {
93
230
  controller.scrollPane(action.pane, { ...(action.delta !== undefined ? { delta: action.delta } : {}), ...(action.offset !== undefined ? { offset: action.offset } : {}) });
94
231
  }
95
232
  catch (error) {
233
+ if (acceptedRunConfirmation) {
234
+ await markArtifactExplorerForCompletedRun("failed");
235
+ }
96
236
  const message = error.message;
97
237
  controller.appendLog(`Web action failed: ${message}`);
98
238
  sendError(client, message, actionId(action));
@@ -105,6 +245,7 @@ export function createWebInteractiveSession(options, webOptions = {}) {
105
245
  }
106
246
  mounted = true;
107
247
  controller.mount();
248
+ void restoreArtifactExplorerFromScope(activeScopeKey);
108
249
  unsubscribe = controller.subscribe((event) => {
109
250
  if (event.type === "log") {
110
251
  server?.broadcast({ type: "log.append", appendedLines: event.appendedLines });
@@ -121,6 +262,7 @@ export function createWebInteractiveSession(options, webOptions = {}) {
121
262
  controller.appendLog(message);
122
263
  },
123
264
  ...(webOptions.openBrowser ? { openBrowser: webOptions.openBrowser } : {}),
265
+ getArtifactCatalog: (input) => artifactCatalogProvider(input),
124
266
  onClientAction: (action, client) => {
125
267
  void dispatch(action, client);
126
268
  },
@@ -166,7 +308,12 @@ export function createWebInteractiveSession(options, webOptions = {}) {
166
308
  requestUserInput: (form) => controller.requestUserInput(form),
167
309
  setSummary: (markdown) => controller.setSummary(markdown),
168
310
  clearSummary: () => controller.clearSummary(),
169
- setScope: (scopeKey, jiraIssueKey, gitBranchName) => controller.setScope(scopeKey, jiraIssueKey, gitBranchName),
311
+ setScope: (scopeKey, jiraIssueKey, gitBranchName) => {
312
+ activeScopeKey = scopeKey;
313
+ controller.setScope(scopeKey, jiraIssueKey, gitBranchName);
314
+ controller.setArtifactExplorerUnavailable();
315
+ void restoreArtifactExplorerFromScope(scopeKey);
316
+ },
170
317
  appendLog: (text) => controller.appendLog(text),
171
318
  setFlowFailed: (flowId) => controller.setFlowFailed(flowId),
172
319
  interruptActiveForm: (message = "Flow interrupted by user.") => {
@@ -12,6 +12,8 @@ const ACTION_TYPES = new Set([
12
12
  "interrupt.openConfirm",
13
13
  "flow.interrupt",
14
14
  "log.clear",
15
+ "artifactExplorer.open",
16
+ "artifactExplorer.close",
15
17
  "help.toggle",
16
18
  "scroll",
17
19
  ]);
@@ -96,7 +98,11 @@ export function parseClientAction(raw) {
96
98
  const action = optionalNonEmptyString(parsed, "action");
97
99
  return { type: "confirm.accept", ...(action ? { action } : {}), ...(actionId ? { actionId } : {}) };
98
100
  }
99
- if (parsed.type === "confirm.cancel" || parsed.type === "form.cancel" || parsed.type === "log.clear") {
101
+ if (parsed.type === "confirm.cancel"
102
+ || parsed.type === "form.cancel"
103
+ || parsed.type === "log.clear"
104
+ || parsed.type === "artifactExplorer.open"
105
+ || parsed.type === "artifactExplorer.close") {
100
106
  return { type: parsed.type, ...(actionId ? { actionId } : {}) };
101
107
  }
102
108
  if (parsed.type === "form.update") {