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()) {