@nauticana/sail 1.1.18 → 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.
- package/README.md +57 -1
- package/package.json +9 -1
- package/src/agent/component/decision_timeline.html +35 -0
- package/src/agent/component/decision_timeline.ts +26 -0
- package/src/agent/component/prompt_language_editor.html +29 -0
- package/src/agent/component/prompt_language_editor.ts +77 -0
- package/src/agent/component/prompt_section_editor.html +93 -0
- package/src/agent/component/prompt_section_editor.ts +170 -0
- package/src/agent/component/release_sections.html +29 -0
- package/src/agent/component/release_sections.ts +32 -0
- package/src/agent/component/turn_event_list.html +101 -0
- package/src/agent/component/turn_event_list.ts +23 -0
- package/src/agent/component/turn_stream_view.html +36 -0
- package/src/agent/component/turn_stream_view.ts +46 -0
- package/src/agent/index.ts +14 -0
- package/src/agent/model/agent_studio.ts +198 -0
- package/src/agent/model/decision.ts +43 -0
- package/src/agent/model/reply_frame.ts +30 -0
- package/src/agent/model/turn_event.ts +95 -0
- package/src/agent/service/agent_studio.service.ts +85 -0
- package/src/agent/service/turn_stream.ts +70 -0
- package/src/agent/util/studio_errors.ts +61 -0
- package/src/agent/util/turn_timeline.ts +115 -0
- package/src/component/dashboard/data_card.html +7 -0
- package/src/component/dashboard/data_card.ts +4 -0
- package/src/component/dashboard/no_source_overlay.html +9 -0
- package/src/component/dashboard/no_source_overlay.ts +21 -0
- package/src/component/table/before_after_diff.html +1 -0
- package/src/component/table/before_after_diff.ts +54 -0
- package/src/component/table/grouped_bulk_action.html +36 -0
- package/src/component/table/grouped_bulk_action.ts +120 -0
- package/src/index.ts +3 -0
- package/src/model/dashboard.ts +1 -1
- package/src/service/base_rest.service.ts +40 -5
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<ol class="turn-events" aria-live="polite">
|
|
2
|
+
@for (item of items(); track $index) {
|
|
3
|
+
@switch (item.kind) {
|
|
4
|
+
@case ('text') {
|
|
5
|
+
<li class="turn-text">{{ item.text }}</li>
|
|
6
|
+
}
|
|
7
|
+
@case ('tool') {
|
|
8
|
+
<li class="turn-tool" [class.turn-tool--error]="item.errorClass !== undefined">
|
|
9
|
+
<div class="turn-tool__head">
|
|
10
|
+
<mat-icon aria-hidden="true">build</mat-icon>
|
|
11
|
+
<span class="turn-tool__id">{{ item.tool_id }}</span>
|
|
12
|
+
<span class="turn-tool__version">{{ item.tool_version }}</span>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
@if (item.approval === 'pending') {
|
|
16
|
+
<p class="turn-tool__approval turn-tool__approval--pending" role="status">Waiting for approval</p>
|
|
17
|
+
} @else if (item.approval === 'resolved') {
|
|
18
|
+
<p class="turn-tool__approval" role="status">Approval: {{ item.decision }}</p>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@if (item.arguments !== undefined) {
|
|
22
|
+
<pre class="turn-tool__arguments">{{ item.arguments | json }}</pre>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@if (item.errorClass !== undefined) {
|
|
26
|
+
<p class="turn-tool__error" role="alert">Tool call failed: {{ item.errorClass }}</p>
|
|
27
|
+
} @else if (item.result) {
|
|
28
|
+
<pre class="turn-tool__output">{{ item.result.output | json }}</pre>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@if (item.effect; as effect) {
|
|
32
|
+
<div
|
|
33
|
+
class="turn-effect"
|
|
34
|
+
[class.turn-effect--satisfied]="effect.observation.status === 'satisfied'"
|
|
35
|
+
[class.turn-effect--violated]="effect.observation.status === 'violated'"
|
|
36
|
+
[class.turn-effect--unknown]="effect.observation.status === 'unknown'"
|
|
37
|
+
>
|
|
38
|
+
@switch (effect.observation.status) {
|
|
39
|
+
@case ('satisfied') {
|
|
40
|
+
<span class="turn-effect__status">Effect confirmed</span>
|
|
41
|
+
}
|
|
42
|
+
@case ('violated') {
|
|
43
|
+
<span class="turn-effect__status" role="alert">Effect not confirmed</span>
|
|
44
|
+
}
|
|
45
|
+
@case ('unknown') {
|
|
46
|
+
<span class="turn-effect__status">Effect could not be verified</span>
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
@if (effect.observation.reconciled) {
|
|
50
|
+
<span class="turn-effect__reconciled">Already in place — nothing was sent again</span>
|
|
51
|
+
}
|
|
52
|
+
@if (effect.observation.reason) {
|
|
53
|
+
<p class="turn-effect__reason">{{ effect.observation.reason }}</p>
|
|
54
|
+
}
|
|
55
|
+
@if (effect.observation.evidence?.length) {
|
|
56
|
+
<ul class="turn-effect__evidence">
|
|
57
|
+
@for (ref of effect.observation.evidence; track ref.URI) {
|
|
58
|
+
<li>
|
|
59
|
+
<span class="turn-ref__uri">{{ ref.URI }}</span>
|
|
60
|
+
<span class="turn-ref__digest">{{ ref.Digest }}</span>
|
|
61
|
+
</li>
|
|
62
|
+
}
|
|
63
|
+
</ul>
|
|
64
|
+
}
|
|
65
|
+
</div>
|
|
66
|
+
}
|
|
67
|
+
</li>
|
|
68
|
+
}
|
|
69
|
+
@case ('evidence') {
|
|
70
|
+
<li class="turn-evidence">
|
|
71
|
+
<ul>
|
|
72
|
+
@for (ref of item.refs; track ref.id) {
|
|
73
|
+
<li>
|
|
74
|
+
<span class="turn-ref__source">{{ ref.source }}</span>
|
|
75
|
+
<span class="turn-ref__uri">{{ ref.uri || ref.object?.URI }}</span>
|
|
76
|
+
@if (ref.object?.Digest) {
|
|
77
|
+
<span class="turn-ref__digest">{{ ref.object?.Digest }}</span>
|
|
78
|
+
}
|
|
79
|
+
</li>
|
|
80
|
+
}
|
|
81
|
+
</ul>
|
|
82
|
+
</li>
|
|
83
|
+
}
|
|
84
|
+
@case ('progress') {
|
|
85
|
+
<li class="turn-progress">
|
|
86
|
+
<span class="turn-progress__stage">{{ item.progress.stage }}</span>
|
|
87
|
+
@if (item.progress.message) {
|
|
88
|
+
<span class="turn-progress__message">{{ item.progress.message }}</span>
|
|
89
|
+
}
|
|
90
|
+
</li>
|
|
91
|
+
}
|
|
92
|
+
@case ('extension') {
|
|
93
|
+
@if (extensionTemplate(); as template) {
|
|
94
|
+
<li class="turn-extension">
|
|
95
|
+
<ng-container [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{ $implicit: item.extension }" />
|
|
96
|
+
</li>
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
</ol>
|
|
@@ -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
|
+
}
|