From f2378216ee30c8f372a038c944e357a76d499eb6 Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Fri, 25 Sep 2026 11:41:13 +0200 Subject: [PATCH] Show which round a loop step is on, and answer for the round seen In an execution, a step on its second round or later carries a Round N badge. Answers to a person's question - decisions, evaluations, evidence, revealing the reference - send the round of its loop the question was opened on, fixed when the dialog opened rather than read at submit time, so an answer to an old round's question is refused by the server instead of being applied to a draft the person never saw. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/app/models/task-execution.ts | 2 ++ .../task-executions-call.base.ts | 16 +++++++--- .../task-executions/task-executions-call.ts | 28 ++++++++++++----- .../task-executions/task-executions.ts | 16 +++++----- .../nodes/task-step-node/task-step-node.css | 12 ++++++++ .../nodes/task-step-node/task-step-node.html | 9 ++++++ .../task-step-node/task-step-node.spec.ts | 25 ++++++++++++++-- .../nodes/task-step-node/task-step-node.ts | 30 +++++++++++++++---- .../task-execution-viewer.ts | 1 + 9 files changed, 110 insertions(+), 29 deletions(-) diff --git a/src/app/models/task-execution.ts b/src/app/models/task-execution.ts index ccb6506..6b1a82e 100644 --- a/src/app/models/task-execution.ts +++ b/src/app/models/task-execution.ts @@ -173,6 +173,8 @@ export type TaskExecutionStep = { outputs?: TaskExecutionStepOutput[]; result?: Record; status: StepStatus; + /** Which round of its loop the step is on; 1, or absent from older servers, for a step in no loop. */ + iteration?: number; started?: boolean; skipReason?: string | null; simulated: boolean; diff --git a/src/app/services/task-executions/task-executions-call.base.ts b/src/app/services/task-executions/task-executions-call.base.ts index 9256ddc..7dc2baf 100644 --- a/src/app/services/task-executions/task-executions-call.base.ts +++ b/src/app/services/task-executions/task-executions-call.base.ts @@ -97,11 +97,16 @@ export abstract class TaskExecutionsCallServiceBase { inputName: string, files: File[] ): Observable; + /** + * `iteration`, when given, is the round of its loop the answer is for: the server refuses it if + * the node has moved on to another, rather than taking it as the answer to a question not seen. + */ abstract submitInteractionText( executionId: string, nodeId: string, fieldName: string, - value: string + value: string, + iteration?: number ): Observable; /** * Submits a human evaluation whole - every criterion and the notes in one call - because the @@ -111,17 +116,20 @@ export abstract class TaskExecutionsCallServiceBase { executionId: string, nodeId: string, verdict: Record, - notes: string + notes: string, + iteration?: number ): Observable; abstract attachEvaluationEvidence( executionId: string, nodeId: string, - files: File[] + files: File[], + iteration?: number ): Observable; /** Asks to see the reference verdict; the execution records that it was seen before judging. */ abstract revealEvaluationReference( executionId: string, - nodeId: string + nodeId: string, + iteration?: number ): Observable; abstract provideAuthorization( executionId: string, diff --git a/src/app/services/task-executions/task-executions-call.ts b/src/app/services/task-executions/task-executions-call.ts index b3c86de..51da7b6 100644 --- a/src/app/services/task-executions/task-executions-call.ts +++ b/src/app/services/task-executions/task-executions-call.ts @@ -269,11 +269,13 @@ export class TaskExecutionsCallService extends TaskExecutionsCallServiceBase { executionId: string, nodeId: string, fieldName: string, - value: string + value: string, + iteration?: number ): Observable { const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/interaction/${encodeURIComponent(fieldName)}/text`; return this.http.put(url, value, { - headers: { 'Content-Type': 'text/plain' } + headers: { 'Content-Type': 'text/plain' }, + params: roundParams(iteration) }).pipe(map((raw) => this.mapExecution(raw))); } @@ -281,28 +283,33 @@ export class TaskExecutionsCallService extends TaskExecutionsCallServiceBase { executionId: string, nodeId: string, verdict: Record, - notes: string + notes: string, + iteration?: number ): Observable { const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/evaluation`; - return this.http.put(url, { verdict, notes }).pipe(map((raw) => this.mapExecution(raw))); + return this.http.put(url, { verdict, notes }, { params: roundParams(iteration) }) + .pipe(map((raw) => this.mapExecution(raw))); } override attachEvaluationEvidence( executionId: string, nodeId: string, - files: File[] + files: File[], + iteration?: number ): Observable { const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/evaluation/evidence`; const formData = new FormData(); for (const file of files) { formData.append('files', file); } - return this.http.put(url, formData).pipe(map((raw) => this.mapExecution(raw))); + return this.http.put(url, formData, { params: roundParams(iteration) }) + .pipe(map((raw) => this.mapExecution(raw))); } - override revealEvaluationReference(executionId: string, nodeId: string): Observable { + override revealEvaluationReference(executionId: string, nodeId: string, iteration?: number): Observable { const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/evaluation/reveal`; - return this.http.put(url, {}).pipe(map((raw) => this.mapExecution(raw))); + return this.http.put(url, {}, { params: roundParams(iteration) }) + .pipe(map((raw) => this.mapExecution(raw))); } override provideAuthorization( @@ -777,3 +784,8 @@ export class TaskExecutionsCallService extends TaskExecutionsCallServiceBase { return Number.isFinite(number) ? number : fallback; } } + +/** The round an answer is for, as the endpoint's optional `iteration` parameter. */ +function roundParams(iteration?: number): Record { + return typeof iteration === 'number' ? { iteration: String(iteration) } : {}; +} diff --git a/src/app/services/task-executions/task-executions.ts b/src/app/services/task-executions/task-executions.ts index 1f27902..23eaabf 100644 --- a/src/app/services/task-executions/task-executions.ts +++ b/src/app/services/task-executions/task-executions.ts @@ -347,14 +347,14 @@ export class TaskExecutionsService { ); } - submitInteractionText(executionId: string, nodeId: string, fieldName: string, value: string) { + submitInteractionText(executionId: string, nodeId: string, fieldName: string, value: string, iteration?: number) { const execution = this._taskExecutions().find((item) => item.id === executionId); if (execution?.interactionSimulationEnabled === true) { return throwError(() => new Error('Manual interaction is disabled for simulated executions.')); } return this.withRefreshAndErrorHandling( - this.taskExecutionsCallService.submitInteractionText(executionId, nodeId, fieldName, value).pipe( + this.taskExecutionsCallService.submitInteractionText(executionId, nodeId, fieldName, value, iteration).pipe( tap((updatedExecution) => { if (updatedExecution.executionKind === 'SUBFLOW') { this.cacheFollowedExecution(updatedExecution); @@ -367,14 +367,14 @@ export class TaskExecutionsService { ); } - submitEvaluation(executionId: string, nodeId: string, verdict: Record, notes: string) { + submitEvaluation(executionId: string, nodeId: string, verdict: Record, notes: string, iteration?: number) { const execution = this._taskExecutions().find((item) => item.id === executionId); if (execution?.interactionSimulationEnabled === true) { return throwError(() => new Error('Manual interaction is disabled for simulated executions.')); } return this.withRefreshAndErrorHandling( - this.taskExecutionsCallService.submitEvaluation(executionId, nodeId, verdict, notes).pipe( + this.taskExecutionsCallService.submitEvaluation(executionId, nodeId, verdict, notes, iteration).pipe( tap((updatedExecution) => { if (updatedExecution.executionKind === 'SUBFLOW') { this.cacheFollowedExecution(updatedExecution); @@ -387,9 +387,9 @@ export class TaskExecutionsService { ); } - attachEvaluationEvidence(executionId: string, nodeId: string, files: File[]) { + attachEvaluationEvidence(executionId: string, nodeId: string, files: File[], iteration?: number) { return this.withRefreshAndErrorHandling( - this.taskExecutionsCallService.attachEvaluationEvidence(executionId, nodeId, files).pipe( + this.taskExecutionsCallService.attachEvaluationEvidence(executionId, nodeId, files, iteration).pipe( tap((execution) => this.replaceExecution(execution)) ), 'Attach evaluation evidence failed', @@ -397,9 +397,9 @@ export class TaskExecutionsService { ); } - revealEvaluationReference(executionId: string, nodeId: string) { + revealEvaluationReference(executionId: string, nodeId: string, iteration?: number) { return this.withRefreshAndErrorHandling( - this.taskExecutionsCallService.revealEvaluationReference(executionId, nodeId).pipe( + this.taskExecutionsCallService.revealEvaluationReference(executionId, nodeId, iteration).pipe( tap((execution) => this.replaceExecution(execution)) ), 'Reveal reference verdict failed', diff --git a/src/app/shared/nodes/task-step-node/task-step-node.css b/src/app/shared/nodes/task-step-node/task-step-node.css index 7e9ee6d..0127a8e 100644 --- a/src/app/shared/nodes/task-step-node/task-step-node.css +++ b/src/app/shared/nodes/task-step-node/task-step-node.css @@ -393,6 +393,18 @@ button.llm-node-bias-summary:focus-visible { font-size: 11px; } +/* A loop's round sits at the other corner, so it does not cover a container's subflow badge. */ +.llm-subflow-badge-wrap:has(.llm-loop-round-badge) { + left: auto; + right: 12px; +} + +.llm-loop-round-badge { + border-color: #c4b5fd; + background: #f5f3ff; + color: #5b21b6; +} + .llm-header { display: flex; align-items: center; diff --git a/src/app/shared/nodes/task-step-node/task-step-node.html b/src/app/shared/nodes/task-step-node/task-step-node.html index dcdbf5a..17b7f3a 100644 --- a/src/app/shared/nodes/task-step-node/task-step-node.html +++ b/src/app/shared/nodes/task-step-node/task-step-node.html @@ -44,6 +44,15 @@ } + @if (loopRound()) { +
+ + + Round {{ loopRound() }} + +
+ } @if (needsAttention() && isHumanNode()) {