@nauticana/sail 1.1.19 → 1.1.20

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.
@@ -0,0 +1,23 @@
1
+ import { ChangeDetectionStrategy, Component, TemplateRef, computed, input } from '@angular/core';
2
+ import { JsonPipe, NgTemplateOutlet } from '@angular/common';
3
+ import { MatIconModule } from '@angular/material/icon';
4
+ import { TurnEvent, TurnExtensionEvent } from '../model/turn_event';
5
+ import { buildTurnTimeline } from '../util/turn_timeline';
6
+
7
+ /**
8
+ * Renders one turn's typed events in frame order. Extension events render only through
9
+ * `extensionTemplate`; without it, and for types it does not handle, they are ignored.
10
+ * Ships no CSS — style the `.turn-*` hooks. Selector: <sail-turn-event-list>.
11
+ */
12
+ @Component({
13
+ selector: 'sail-turn-event-list',
14
+ changeDetection: ChangeDetectionStrategy.OnPush,
15
+ imports: [JsonPipe, NgTemplateOutlet, MatIconModule],
16
+ templateUrl: './turn_event_list.html',
17
+ })
18
+ export class TurnEventListComponent {
19
+ readonly events = input.required<readonly TurnEvent[]>();
20
+ readonly extensionTemplate = input<TemplateRef<{ $implicit: TurnExtensionEvent }>>();
21
+
22
+ protected readonly items = computed(() => buildTurnTimeline(this.events()));
23
+ }
@@ -0,0 +1,36 @@
1
+ <div class="turn-stream" [attr.data-status]="status()">
2
+ <sail-turn-event-list [events]="events()" [extensionTemplate]="extensionTemplate()" />
3
+
4
+ @if (storedPayload() !== undefined) {
5
+ @if (isText(storedPayload())) {
6
+ <p class="turn-stream__stored">{{ storedPayload() }}</p>
7
+ } @else {
8
+ <pre class="turn-stream__stored">{{ storedPayload() | json }}</pre>
9
+ }
10
+ }
11
+
12
+ @switch (status()) {
13
+ @case ('streaming') {
14
+ <mat-progress-bar mode="indeterminate" aria-label="Agent is responding" />
15
+ }
16
+ @case ('awaiting_approval') {
17
+ <p class="turn-stream__status" role="status">Waiting for approval. The conversation continues once it is decided.</p>
18
+ }
19
+ @case ('cancelling') {
20
+ <p class="turn-stream__status" role="status">Cancelling…</p>
21
+ }
22
+ @case ('canceled') {
23
+ <p class="turn-stream__status" role="status">Canceled</p>
24
+ }
25
+ @case ('failed') {
26
+ <p class="turn-stream__status turn-stream__status--failed" role="alert">Turn failed: {{ finalFrame()?.error_code }}</p>
27
+ }
28
+ @case ('replay_expired') {
29
+ <p class="turn-stream__status" role="status">Loading the stored result…</p>
30
+ }
31
+ }
32
+
33
+ @if (canCancel()) {
34
+ <button matButton type="button" (click)="requestCancel()">Cancel</button>
35
+ }
36
+ </div>
@@ -0,0 +1,46 @@
1
+ import { ChangeDetectionStrategy, Component, TemplateRef, computed, input, output } from '@angular/core';
2
+ import { JsonPipe } from '@angular/common';
3
+ import { MatButtonModule } from '@angular/material/button';
4
+ import { MatProgressBarModule } from '@angular/material/progress-bar';
5
+ import { TurnExtensionEvent } from '../model/turn_event';
6
+ import { TurnStream } from '../service/turn_stream';
7
+ import { TurnEventListComponent } from './turn_event_list';
8
+
9
+ /**
10
+ * One turn's stream: its events, its status, and a cancel request. The app owns the transport
11
+ * and the cancel call; on `cancelRequested` it sends the request and calls
12
+ * `stream.markCancelRequested()`. Selector: <sail-turn-stream-view>.
13
+ */
14
+ @Component({
15
+ selector: 'sail-turn-stream-view',
16
+ changeDetection: ChangeDetectionStrategy.OnPush,
17
+ imports: [JsonPipe, MatButtonModule, MatProgressBarModule, TurnEventListComponent],
18
+ templateUrl: './turn_stream_view.html',
19
+ })
20
+ export class TurnStreamViewComponent {
21
+ readonly stream = input.required<TurnStream>();
22
+ readonly extensionTemplate = input<TemplateRef<{ $implicit: TurnExtensionEvent }>>();
23
+
24
+ readonly cancelRequested = output<string>();
25
+
26
+ protected readonly status = computed(() => this.stream().status());
27
+ protected readonly events = computed(() => this.stream().events());
28
+ protected readonly finalFrame = computed(() => this.stream().finalFrame());
29
+ protected readonly canCancel = computed(() => {
30
+ const status = this.status();
31
+ return status === 'streaming' || status === 'awaiting_approval';
32
+ });
33
+ /** A synthesized final frame carries the whole answer in its payload and no events. */
34
+ protected readonly storedPayload = computed(() => {
35
+ const frame = this.finalFrame();
36
+ return frame && !frame.error_code && this.events().length === 0 ? frame.payload : undefined;
37
+ });
38
+
39
+ protected isText(payload: unknown): payload is string {
40
+ return typeof payload === 'string';
41
+ }
42
+
43
+ protected requestCancel(): void {
44
+ this.cancelRequested.emit(this.stream().requestId);
45
+ }
46
+ }
@@ -0,0 +1,14 @@
1
+ export * from './model/agent_studio';
2
+ export * from './model/turn_event';
3
+ export * from './model/reply_frame';
4
+ export * from './model/decision';
5
+ export * from './service/agent_studio.service';
6
+ export * from './service/turn_stream';
7
+ export * from './util/studio_errors';
8
+ export * from './util/turn_timeline';
9
+ export * from './component/prompt_section_editor';
10
+ export * from './component/prompt_language_editor';
11
+ export * from './component/release_sections';
12
+ export * from './component/turn_event_list';
13
+ export * from './component/turn_stream_view';
14
+ export * from './component/decision_timeline';
@@ -0,0 +1,198 @@
1
+ /** Agent Studio `studio-v2` wire types. Field names are the backend's JSON names. */
2
+
3
+ export type AgentMergeMode = 'append' | 'replace';
4
+ export type AgentResetScope = 'agent_override' | 'type_default' | 'platform_baseline';
5
+
6
+ export interface AgentSummary {
7
+ agent_type: string;
8
+ agent_name: string;
9
+ display_name: string;
10
+ purpose: string;
11
+ enabled: boolean;
12
+ is_default: boolean;
13
+ readiness: string;
14
+ readiness_reason: string;
15
+ type_defaults_revision: number;
16
+ agent_revision: number;
17
+ published_version: number;
18
+ published_at?: string;
19
+ last_run_at?: string;
20
+ }
21
+
22
+ export interface AgentFieldError {
23
+ field: string;
24
+ message: string;
25
+ }
26
+
27
+ export interface AgentValidationProblem {
28
+ message: string;
29
+ fields: AgentFieldError[];
30
+ }
31
+
32
+ export interface AgentModelSelection {
33
+ text_model: string;
34
+ image_model: string;
35
+ video_model: string;
36
+ }
37
+
38
+ export interface AgentApprovalPolicy {
39
+ require_approval: boolean;
40
+ }
41
+
42
+ /** One contribution to a prompt section. `editable` is response-only. */
43
+ export interface AgentPromptLayer {
44
+ scope_id: string;
45
+ scope_kind: string;
46
+ merge_mode: AgentMergeMode;
47
+ sealed: boolean;
48
+ editable: boolean;
49
+ instruction: string;
50
+ output: string;
51
+ }
52
+
53
+ export interface AgentPromptSection {
54
+ prompt_header_id: number;
55
+ caption: string;
56
+ description: string;
57
+ /** Widest scope first; the order is the server's and is never re-sorted. */
58
+ layers: AgentPromptLayer[];
59
+ effective_text: string;
60
+ effective_output: string;
61
+ }
62
+
63
+ export interface AgentLanguageDraft {
64
+ language_code: string;
65
+ prompt_sections: AgentPromptSection[];
66
+ }
67
+
68
+ export interface AgentDrift {
69
+ active_version: number;
70
+ changed_languages: string[];
71
+ causes: string[];
72
+ }
73
+
74
+ export interface AgentDraft {
75
+ agent_type: string;
76
+ agent_name: string;
77
+ display_name: string;
78
+ enabled: boolean;
79
+ is_default: boolean;
80
+ approval_policy: AgentApprovalPolicy;
81
+ models: AgentModelSelection;
82
+ languages: AgentLanguageDraft[];
83
+ agent_scope_id?: string;
84
+ type_scope_id?: string;
85
+ drift?: AgentDrift;
86
+ expected_type_defaults_revision: number;
87
+ expected_agent_revision: number;
88
+ }
89
+
90
+ export interface AgentTestRequest {
91
+ agent_name: string;
92
+ language_code: string;
93
+ task: string;
94
+ input_data: string;
95
+ }
96
+
97
+ export interface AgentTestResult {
98
+ agent_name: string;
99
+ language_code: string;
100
+ model: string;
101
+ digest: string;
102
+ output: string;
103
+ latency_ms: number;
104
+ input_tokens: number;
105
+ output_tokens: number;
106
+ credits: number;
107
+ sections: string[];
108
+ }
109
+
110
+ export interface AgentPublishRequest {
111
+ agent_name: string;
112
+ change_summary: string;
113
+ expected_agent_revision: number;
114
+ expected_type_defaults_revision: number;
115
+ }
116
+
117
+ export interface AgentRestoreRequest {
118
+ agent_name: string;
119
+ version: number;
120
+ }
121
+
122
+ /** `prompt_header_id` 0 and `language_code` '' leave that dimension unnarrowed. */
123
+ export interface AgentResetRequest {
124
+ agent_name: string;
125
+ scope: AgentResetScope;
126
+ prompt_header_id: number;
127
+ language_code: string;
128
+ expected_agent_revision: number;
129
+ expected_type_defaults_revision: number;
130
+ }
131
+
132
+ export interface AgentSetDefaultRequest {
133
+ agent_name: string;
134
+ expected_type_defaults_revision: number;
135
+ }
136
+
137
+ export interface AgentSetEnabledRequest {
138
+ agent_name: string;
139
+ enabled: boolean;
140
+ expected_agent_revision: number;
141
+ }
142
+
143
+ export interface AgentEnabledState {
144
+ enabled: boolean;
145
+ expected_agent_revision: number;
146
+ }
147
+
148
+ export interface AgentRelease {
149
+ agent_name: string;
150
+ agent_type: string;
151
+ version: number;
152
+ enabled: boolean;
153
+ models: AgentModelSelection;
154
+ require_approval: boolean;
155
+ definition_digest: string;
156
+ change_summary: string;
157
+ published_by: number;
158
+ published_at: string;
159
+ active: boolean;
160
+ languages: string[];
161
+ }
162
+
163
+ export interface AgentAuditEvent {
164
+ event: string;
165
+ detail: string;
166
+ user_id: number;
167
+ event_time: string;
168
+ }
169
+
170
+ /** The layer that decided a section when its release was compiled. */
171
+ export interface AgentPromptSource {
172
+ scope_id: string;
173
+ scope_kind: string;
174
+ merge_mode: AgentMergeMode;
175
+ sealed: boolean;
176
+ }
177
+
178
+ export interface AgentReleaseSection {
179
+ language_code: string;
180
+ prompt_header_id: number;
181
+ caption: string;
182
+ description: string;
183
+ instruction: string;
184
+ output: string;
185
+ sequence: number;
186
+ source: AgentPromptSource;
187
+ }
188
+
189
+ export interface StudioModel {
190
+ id: string;
191
+ provider: string;
192
+ model_type: string;
193
+ display_name: string;
194
+ input_credits_per_1k: number;
195
+ output_credits_per_1k: number;
196
+ image_credits: number;
197
+ video_credits_per_sec: number;
198
+ }
@@ -0,0 +1,43 @@
1
+ import { ObjectRef } from './turn_event';
2
+
3
+ export type DecisionOutcome = 'allow' | 'deny' | 'pending';
4
+
5
+ export interface PrincipalRef {
6
+ kind: string;
7
+ id: string;
8
+ }
9
+
10
+ export interface AuthorityRef {
11
+ subject: PrincipalRef;
12
+ grant_id?: string;
13
+ grantor?: PrincipalRef;
14
+ }
15
+
16
+ /**
17
+ * One audit record as sail renders it. The audit endpoint is the app's, so the app maps its
18
+ * response onto this shape; `id` is whatever key the endpoint gives a record.
19
+ */
20
+ export interface DecisionRecord {
21
+ id: string | number;
22
+ category: string;
23
+ action: string;
24
+ resource: string;
25
+ outcome: DecisionOutcome;
26
+ reason: string;
27
+ principal: PrincipalRef;
28
+ authority?: AuthorityRef;
29
+ scope_id?: string;
30
+ release_version?: string;
31
+ policy_id?: string;
32
+ policy_version?: string;
33
+ obligations?: string[];
34
+ evidence?: ObjectRef;
35
+ request_id: string;
36
+ conversation_id?: string;
37
+ occurred_at: string;
38
+ }
39
+
40
+ export interface DecisionCategoryGroup {
41
+ category: string;
42
+ records: DecisionRecord[];
43
+ }
@@ -0,0 +1,30 @@
1
+ import { TurnEvent } from './turn_event';
2
+
3
+ export const TURN_ERROR_CANCELED = 'canceled';
4
+
5
+ /**
6
+ * One reply frame as sail consumes it. The backend's reply type fixes no JSON names, so the
7
+ * app's transport adapter maps its own wire shape onto this one.
8
+ */
9
+ export interface TurnReplyFrame {
10
+ request_id: string;
11
+ conversation_id: string;
12
+ sequence: number;
13
+ events: TurnEvent[];
14
+ final: boolean;
15
+ error_code?: string;
16
+ /** Opaque product payload; on a synthesized final frame it is the whole answer. */
17
+ payload?: unknown;
18
+ agent_version?: string;
19
+ emitted_at?: string;
20
+ }
21
+
22
+ export type TurnStreamStatus =
23
+ | 'idle'
24
+ | 'streaming'
25
+ | 'awaiting_approval'
26
+ | 'cancelling'
27
+ | 'completed'
28
+ | 'canceled'
29
+ | 'failed'
30
+ | 'replay_expired';
@@ -0,0 +1,95 @@
1
+ /** Typed turn events (`v` 1). Exactly the member a `kind` names is set. */
2
+
3
+ export const TURN_EVENT_KINDS = [
4
+ 'text_delta',
5
+ 'tool_proposal',
6
+ 'tool_result',
7
+ 'approval_pending',
8
+ 'approval_resolved',
9
+ 'evidence',
10
+ 'effect',
11
+ 'progress',
12
+ 'result',
13
+ 'extension',
14
+ ] as const;
15
+
16
+ export type TurnEventKind = (typeof TURN_EVENT_KINDS)[number];
17
+ export type EffectStatus = 'satisfied' | 'violated' | 'unknown';
18
+
19
+ /** A reference to stored content, never the content itself. */
20
+ export interface ObjectRef {
21
+ URI: string;
22
+ Digest: string;
23
+ }
24
+
25
+ export interface TurnToolEvent {
26
+ call_id: string;
27
+ tool_id: string;
28
+ tool_version: string;
29
+ arguments?: unknown;
30
+ output?: unknown;
31
+ is_error?: boolean;
32
+ }
33
+
34
+ export interface EffectObservation {
35
+ status: EffectStatus;
36
+ reason?: string;
37
+ observed_at?: string;
38
+ evidence?: ObjectRef[];
39
+ /** The effect already held, so the mutation was not sent again. */
40
+ reconciled?: boolean;
41
+ }
42
+
43
+ export interface TurnEffectEvent {
44
+ call_id: string;
45
+ tool_id: string;
46
+ tool_version: string;
47
+ observation: EffectObservation;
48
+ }
49
+
50
+ export interface TurnApprovalEvent {
51
+ call_id?: string;
52
+ tool_id?: string;
53
+ decision?: string;
54
+ }
55
+
56
+ export interface EvidenceRef {
57
+ id: string;
58
+ source: string;
59
+ object?: ObjectRef;
60
+ call_id?: string;
61
+ uri?: string;
62
+ }
63
+
64
+ export interface TurnProgressEvent {
65
+ stage: string;
66
+ iteration?: number;
67
+ message?: string;
68
+ }
69
+
70
+ export interface TurnExtensionEvent {
71
+ type: string;
72
+ v: number;
73
+ payload?: unknown;
74
+ }
75
+
76
+ export interface TurnEvent {
77
+ v: number;
78
+ kind: TurnEventKind;
79
+ text?: string;
80
+ tool?: TurnToolEvent;
81
+ approval?: TurnApprovalEvent;
82
+ evidence?: EvidenceRef[];
83
+ effect?: TurnEffectEvent;
84
+ progress?: TurnProgressEvent;
85
+ extension?: TurnExtensionEvent;
86
+ }
87
+
88
+ const knownKinds: ReadonlySet<string> = new Set(TURN_EVENT_KINDS);
89
+
90
+ /** Narrows a received frame member; unknown kinds are dropped by callers, not reported. */
91
+ export function isKnownTurnEvent(value: unknown): value is TurnEvent {
92
+ if (!value || typeof value !== 'object') return false;
93
+ const kind = (value as { kind?: unknown }).kind;
94
+ return typeof kind === 'string' && knownKinds.has(kind);
95
+ }
@@ -0,0 +1,85 @@
1
+ import { Injectable } from '@angular/core';
2
+ import { Observable } from 'rxjs';
3
+ import { BaseRestService } from '../../service/base_rest.service';
4
+ import {
5
+ AgentAuditEvent,
6
+ AgentDraft,
7
+ AgentEnabledState,
8
+ AgentPublishRequest,
9
+ AgentRelease,
10
+ AgentReleaseSection,
11
+ AgentResetRequest,
12
+ AgentRestoreRequest,
13
+ AgentSetDefaultRequest,
14
+ AgentSetEnabledRequest,
15
+ AgentSummary,
16
+ AgentTestRequest,
17
+ AgentTestResult,
18
+ StudioModel,
19
+ } from '../model/agent_studio';
20
+
21
+ const studioBasePath = '/api/agent-studio/';
22
+
23
+ /** Client for the `studio-v2` Agent Studio routes, which are fixed and unversioned. */
24
+ @Injectable({ providedIn: 'root' })
25
+ export class AgentStudioService extends BaseRestService {
26
+ listAgents(): Observable<AgentSummary[]> {
27
+ return this.http.get<AgentSummary[]>(this.studioUrl('agents'));
28
+ }
29
+
30
+ getDraft(agentName: string): Observable<AgentDraft> {
31
+ return this.http.get<AgentDraft>(this.studioUrl('agent'), { params: { agent_name: agentName } });
32
+ }
33
+
34
+ /** Every editable layer must be sent back: one missing from `draft` ends its binding. */
35
+ saveDraft(draft: AgentDraft): Observable<AgentDraft> {
36
+ return this.http.post<AgentDraft>(this.studioUrl('draft'), draft);
37
+ }
38
+
39
+ setEnabled(request: AgentSetEnabledRequest): Observable<AgentEnabledState> {
40
+ return this.http.post<AgentEnabledState>(this.studioUrl('enabled'), request);
41
+ }
42
+
43
+ /** Runs the saved draft, not unsaved form content. */
44
+ testDraft(request: AgentTestRequest): Observable<AgentTestResult> {
45
+ return this.http.post<AgentTestResult>(this.studioUrl('test'), request);
46
+ }
47
+
48
+ publish(request: AgentPublishRequest): Observable<AgentRelease> {
49
+ return this.http.post<AgentRelease>(this.studioUrl('publish'), request);
50
+ }
51
+
52
+ restore(request: AgentRestoreRequest): Observable<AgentRelease> {
53
+ return this.http.post<AgentRelease>(this.studioUrl('restore'), request);
54
+ }
55
+
56
+ reset(request: AgentResetRequest): Observable<AgentDraft> {
57
+ return this.http.post<AgentDraft>(this.studioUrl('reset'), request);
58
+ }
59
+
60
+ setDefault(request: AgentSetDefaultRequest): Observable<AgentSummary[]> {
61
+ return this.http.post<AgentSummary[]>(this.studioUrl('set-default'), request);
62
+ }
63
+
64
+ history(agentName: string): Observable<AgentRelease[]> {
65
+ return this.http.get<AgentRelease[]>(this.studioUrl('history'), { params: { agent_name: agentName } });
66
+ }
67
+
68
+ auditLog(agentName: string): Observable<AgentAuditEvent[]> {
69
+ return this.http.get<AgentAuditEvent[]>(this.studioUrl('audit'), { params: { agent_name: agentName } });
70
+ }
71
+
72
+ releaseSections(agentName: string, version: number): Observable<AgentReleaseSection[]> {
73
+ return this.http.get<AgentReleaseSection[]>(this.studioUrl('release-sections'), {
74
+ params: { agent_name: agentName, version: String(version) },
75
+ });
76
+ }
77
+
78
+ models(): Observable<StudioModel[]> {
79
+ return this.http.get<StudioModel[]>(this.studioUrl('models'));
80
+ }
81
+
82
+ private studioUrl(route: string): string {
83
+ return this.url(studioBasePath + route);
84
+ }
85
+ }
@@ -0,0 +1,70 @@
1
+ import { computed, signal } from '@angular/core';
2
+ import { TURN_ERROR_CANCELED, TurnReplyFrame, TurnStreamStatus } from '../model/reply_frame';
3
+ import { TurnEvent } from '../model/turn_event';
4
+
5
+ const terminalStatuses: ReadonlySet<TurnStreamStatus> = new Set(['completed', 'canceled', 'failed']);
6
+
7
+ /**
8
+ * Client-side state of one turn's reply stream. Transport stays the app's: it feeds frames to
9
+ * `accept`, reconnects from `cursor()`, and reports an expired replay window. One instance per
10
+ * turn; not injectable.
11
+ */
12
+ export class TurnStream {
13
+ private readonly frames = signal<TurnReplyFrame[]>([]);
14
+ private readonly state = signal<TurnStreamStatus>('idle');
15
+ private readonly lastSequence = signal(0);
16
+
17
+ readonly status = this.state.asReadonly();
18
+ /** Last sequence rendered; a reconnect resumes after it. */
19
+ readonly cursor = this.lastSequence.asReadonly();
20
+ readonly events = computed<TurnEvent[]>(() => this.frames().flatMap((frame) => frame.events));
21
+ readonly finalFrame = computed(() => this.frames().find((frame) => frame.final));
22
+ readonly isTerminal = computed(() => terminalStatuses.has(this.state()));
23
+
24
+ constructor(readonly requestId: string) {}
25
+
26
+ /** Returns false for a frame of another turn, one already rendered, or one after the final. */
27
+ accept(frame: TurnReplyFrame): boolean {
28
+ if (frame.request_id !== this.requestId || this.isTerminal()) return false;
29
+ if (frame.sequence <= this.lastSequence()) return false;
30
+ this.lastSequence.set(frame.sequence);
31
+ this.append(frame);
32
+ return true;
33
+ }
34
+
35
+ /** The reconnect cursor fell behind the retained window; the app now reads the stored result. */
36
+ markReplayExpired(): void {
37
+ if (!this.isTerminal()) this.state.set('replay_expired');
38
+ }
39
+
40
+ /**
41
+ * A final frame rebuilt from the turn record: a payload or an error code, no intermediate
42
+ * events. It completes the turn whatever its sequence.
43
+ */
44
+ acceptStoredResult(frame: TurnReplyFrame): boolean {
45
+ if (frame.request_id !== this.requestId || !frame.final || this.isTerminal()) return false;
46
+ this.append(frame);
47
+ return true;
48
+ }
49
+
50
+ /** Cancel is a request: the turn stays `cancelling` until its final frame arrives. */
51
+ markCancelRequested(): void {
52
+ if (!this.isTerminal()) this.state.set('cancelling');
53
+ }
54
+
55
+ private append(frame: TurnReplyFrame): void {
56
+ this.frames.update((frames) => [...frames, frame]);
57
+ this.state.set(this.statusAfter(frame));
58
+ }
59
+
60
+ private statusAfter(frame: TurnReplyFrame): TurnStreamStatus {
61
+ if (frame.final) {
62
+ if (frame.error_code === TURN_ERROR_CANCELED) return 'canceled';
63
+ return frame.error_code ? 'failed' : 'completed';
64
+ }
65
+ if (this.state() === 'cancelling') return 'cancelling';
66
+ // approval_pending ends the delivery, not the turn: resumed frames follow on the same stream.
67
+ const last = frame.events[frame.events.length - 1];
68
+ return last?.kind === 'approval_pending' ? 'awaiting_approval' : 'streaming';
69
+ }
70
+ }