@nauticana/sail 1.1.33 → 1.1.35

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.
Files changed (61) hide show
  1. package/README.md +36 -23
  2. package/package.json +1 -1
  3. package/src/agent/component/approval_inbox.html +110 -0
  4. package/src/agent/component/approval_inbox.ts +129 -0
  5. package/src/agent/component/assistant_activity.html +28 -0
  6. package/src/agent/component/assistant_activity.ts +40 -0
  7. package/src/agent/component/decision_timeline.html +4 -4
  8. package/src/agent/component/decision_timeline.ts +2 -0
  9. package/src/agent/component/prompt_section_editor.html +4 -3
  10. package/src/agent/component/prompt_section_editor.ts +12 -5
  11. package/src/agent/component/release_sections.html +1 -1
  12. package/src/agent/component/release_sections.ts +2 -1
  13. package/src/agent/component/studio_page.html +303 -0
  14. package/src/agent/component/studio_page.ts +403 -0
  15. package/src/agent/component/studio_tab.ts +14 -0
  16. package/src/agent/component/turn_event_list.html +9 -0
  17. package/src/agent/component/turn_thread.html +27 -0
  18. package/src/agent/component/turn_thread.ts +83 -0
  19. package/src/agent/index.ts +8 -0
  20. package/src/agent/model/agent_studio.ts +185 -9
  21. package/src/agent/model/approval.ts +78 -0
  22. package/src/agent/model/conversation.ts +40 -1
  23. package/src/agent/model/decision.ts +14 -9
  24. package/src/agent/model/turn_event.ts +10 -0
  25. package/src/agent/service/agent_studio.service.ts +65 -1
  26. package/src/agent/service/approval.service.ts +38 -0
  27. package/src/agent/service/conversation.service.ts +10 -0
  28. package/src/agent/util/release_diff.ts +33 -0
  29. package/src/agent/util/studio_errors.ts +11 -0
  30. package/src/agent/util/turn_timeline.ts +7 -1
  31. package/src/component/agency/agency_earnings_summary.html +3 -3
  32. package/src/component/agency/agency_earnings_summary.ts +2 -2
  33. package/src/component/agency/agency_payout_history.html +1 -1
  34. package/src/component/agency/agency_payout_history.ts +2 -3
  35. package/src/component/agency/commission_ledger.html +1 -1
  36. package/src/component/agency/commission_ledger.ts +2 -3
  37. package/src/component/billing/plan_selector.ts +1 -1
  38. package/src/component/billing/price_selector.ts +5 -5
  39. package/src/component/login/account_deletion.html +17 -4
  40. package/src/component/login/account_deletion.ts +35 -28
  41. package/src/component/login/social_login.ts +6 -5
  42. package/src/component/payout/bank_info_form.html +15 -6
  43. package/src/component/payout/bank_info_form.ts +28 -72
  44. package/src/component/security/api_keys.html +78 -0
  45. package/src/component/security/api_keys.ts +90 -0
  46. package/src/component/security/connected_apps.html +25 -0
  47. package/src/component/security/connected_apps.ts +40 -0
  48. package/src/index.ts +8 -0
  49. package/src/model/access.ts +28 -0
  50. package/src/model/appdata.ts +41 -67
  51. package/src/model/auth.ts +17 -2
  52. package/src/model/geo.ts +12 -0
  53. package/src/service/access.service.ts +57 -0
  54. package/src/service/auth.service.ts +10 -4
  55. package/src/service/caption_pipe.ts +32 -0
  56. package/src/service/currency.service.ts +51 -0
  57. package/src/service/label.service.ts +5 -4
  58. package/src/service/money_pipe.ts +27 -0
  59. package/src/service/rest_service.ts +1 -1
  60. package/src/service/rest_url.ts +4 -0
  61. package/src/util/money.ts +18 -34
@@ -6,7 +6,7 @@
6
6
  <h4 class="release-section__caption">{{ section.caption }}</h4>
7
7
  <p class="release-section__source">
8
8
  Decided by {{ section.source.scope_kind }} scope {{ section.source.scope_id }}
9
- ({{ section.source.merge_mode }})
9
+ ({{ section.source.merge_mode | sailCaption:'config_merge_mode' }})
10
10
  @if (section.source.sealed) {
11
11
  <span class="release-section__sealed">
12
12
  <mat-icon aria-hidden="true">lock</mat-icon>
@@ -1,5 +1,6 @@
1
1
  import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
2
2
  import { MatIconModule } from '@angular/material/icon';
3
+ import { CaptionPipe } from '../../service/caption_pipe';
3
4
  import { AgentReleaseSection } from '../model/agent_studio';
4
5
 
5
6
  interface ReleaseLanguageGroup {
@@ -14,7 +15,7 @@ interface ReleaseLanguageGroup {
14
15
  @Component({
15
16
  selector: 'sail-release-sections',
16
17
  changeDetection: ChangeDetectionStrategy.OnPush,
17
- imports: [MatIconModule],
18
+ imports: [MatIconModule, CaptionPipe],
18
19
  templateUrl: './release_sections.html',
19
20
  })
20
21
  export class ReleaseSectionsComponent {
@@ -0,0 +1,303 @@
1
+ <div class="studio">
2
+ <aside class="studio__rail" aria-label="Agents">
3
+ <mat-slide-toggle [checked]="showArchived()" (change)="showArchived.set($event.checked)">Show archived</mat-slide-toggle>
4
+ @for (group of groups(); track group.name) {
5
+ <section class="studio__group" [attr.aria-label]="group.name">
6
+ <h3 class="studio__group-name">{{ group.name }}</h3>
7
+ <ul>
8
+ @for (agent of group.agents; track agent.agent_name) {
9
+ <li>
10
+ <button type="button" class="studio__agent" [class.selected]="agent.agent_name === selectedName()"
11
+ [attr.aria-current]="agent.agent_name === selectedName()" (click)="select(agent.agent_name)">
12
+ <span class="studio__agent-name">{{ agent.display_name }}</span>
13
+ <span [class]="'studio__readiness studio__readiness--' + agent.readiness">{{ readinessCaption(agent.readiness) }}</span>
14
+ <span class="studio__agent-meta">
15
+ @if (agent.is_default) { <span>Default</span> }
16
+ @if (agent.archived) { <span>Archived</span> }
17
+ @if (agent.published_version > 0) { <span>v{{ agent.published_version }}</span> } @else { <span>Never published</span> }
18
+ </span>
19
+ </button>
20
+ </li>
21
+ }
22
+ </ul>
23
+ </section>
24
+ }
25
+ </aside>
26
+
27
+ <main class="studio__main">
28
+ @if (draft(); as d) {
29
+ <header class="studio__header">
30
+ <h2>{{ d.display_name }}</h2>
31
+ @if (selected(); as agent) {
32
+ <p>{{ agent.purpose }} @if (agent.readiness_reason) { · {{ agent.readiness_reason }} }</p>
33
+ }
34
+ @if (d.drift; as drift) {
35
+ <p class="studio__drift" role="status">
36
+ {{ drift.causes.join(', ') }} changed since version {{ drift.active_version }} ({{ drift.changed_languages.join(', ') }}). Publishing adopts it.
37
+ </p>
38
+ }
39
+ </header>
40
+
41
+ <nav class="studio__tabs" role="tablist" aria-label="Agent">
42
+ @for (t of tabs(); track t.id) {
43
+ <button type="button" role="tab" [class.active]="tab() === t.id" [attr.aria-selected]="tab() === t.id"
44
+ (click)="openTab(t.id)">{{ t.label }}</button>
45
+ }
46
+ </nav>
47
+
48
+ <div class="studio__panel" role="tabpanel">
49
+ @switch (tab()) {
50
+ @case ('overview') {
51
+ <mat-form-field>
52
+ <mat-label>Display name</mat-label>
53
+ <input matInput [value]="d.display_name" [disabled]="!can('EDIT')"
54
+ (input)="patchDraft({ display_name: $any($event.target).value })">
55
+ </mat-form-field>
56
+ <mat-slide-toggle [checked]="d.enabled" [disabled]="!can('EDIT') || busy()"
57
+ (change)="setEnabled($event.checked)">Enabled</mat-slide-toggle>
58
+ <mat-form-field>
59
+ <mat-label>Text model</mat-label>
60
+ <mat-select [value]="d.models.text_model" [disabled]="!can('EDIT')" (selectionChange)="patchModel('text_model', $event.value)">
61
+ @for (m of modelsOf('T'); track m.id) { <mat-option [value]="m.id">{{ m.display_name }} · {{ m.provider }}</mat-option> }
62
+ </mat-select>
63
+ </mat-form-field>
64
+ @if (modelsOf('I').length > 0) {
65
+ <mat-form-field>
66
+ <mat-label>Image model</mat-label>
67
+ <mat-select [value]="d.models.image_model" [disabled]="!can('EDIT')" (selectionChange)="patchModel('image_model', $event.value)">
68
+ <mat-option value="">None</mat-option>
69
+ @for (m of modelsOf('I'); track m.id) { <mat-option [value]="m.id">{{ m.display_name }} · {{ m.provider }}</mat-option> }
70
+ </mat-select>
71
+ </mat-form-field>
72
+ }
73
+ @if (modelsOf('V').length > 0) {
74
+ <mat-form-field>
75
+ <mat-label>Video model</mat-label>
76
+ <mat-select [value]="d.models.video_model" [disabled]="!can('EDIT')" (selectionChange)="patchModel('video_model', $event.value)">
77
+ <mat-option value="">None</mat-option>
78
+ @for (m of modelsOf('V'); track m.id) { <mat-option [value]="m.id">{{ m.display_name }} · {{ m.provider }}</mat-option> }
79
+ </mat-select>
80
+ </mat-form-field>
81
+ }
82
+ <p class="studio__key">Internal key <code>{{ d.agent_name }}</code> · type {{ d.agent_type }}</p>
83
+ <div class="studio__actions">
84
+ @if (!d.is_default && can('PUBLISH')) {
85
+ <button matButton type="button" [disabled]="busy()" (click)="makeDefault()">Make default {{ d.agent_type }}</button>
86
+ }
87
+ @if (can('ARCHIVE')) {
88
+ <button matButton type="button" [disabled]="busy()" (click)="setArchived(!selected()?.archived)">
89
+ {{ selected()?.archived ? 'Unarchive' : 'Archive' }}
90
+ </button>
91
+ }
92
+ </div>
93
+ @if (can('CREATE', 'agent_type:' + d.agent_type)) {
94
+ <div class="studio__create">
95
+ <mat-form-field>
96
+ <mat-label>New {{ d.agent_type }} agent, copied from this one</mat-label>
97
+ <input matInput #newName>
98
+ </mat-form-field>
99
+ <button matButton type="button" [disabled]="busy() || dirty()" (click)="createCopy(newName.value); newName.value = ''">Create</button>
100
+ </div>
101
+ }
102
+ }
103
+ @case ('prompts') {
104
+ @if (d.languages.length > 1) {
105
+ <mat-form-field>
106
+ <mat-label>Language</mat-label>
107
+ <mat-select [value]="currentLanguage()?.language_code" (selectionChange)="language.set($event.value)">
108
+ @for (l of d.languages; track l.language_code) { <mat-option [value]="l.language_code">{{ l.language_code }}</mat-option> }
109
+ </mat-select>
110
+ </mat-form-field>
111
+ }
112
+ @if (currentLanguage(); as lang) {
113
+ <sail-prompt-language-editor [language]="lang" [agentScopeId]="d.agent_scope_id ?? ''" [typeScopeId]="d.type_scope_id ?? ''"
114
+ [disabled]="!can('EDIT') || busy()" (languageChange)="updateLanguage($event)"
115
+ (validChange)="promptsValid.set($event)" (resetRequested)="reset($event)" />
116
+ }
117
+ }
118
+ @case ('settings') {
119
+ @if (settings(); as s) {
120
+ @if (s.problem) { <p class="studio__error" role="alert">{{ s.problem }}</p> }
121
+ <h3>In effect</h3>
122
+ <ul class="studio__settings">
123
+ @for (e of s.effective; track e.resource_kind + '/' + e.resource_id) {
124
+ <li>
125
+ <strong>{{ e.resource_kind }} / {{ e.resource_id }}</strong>
126
+ from {{ e.source.scope_kind }} ({{ e.source.merge_mode | sailCaption:'config_merge_mode' }}@if (e.source.sealed) {, sealed})
127
+ <pre>{{ e.value | json }}</pre>
128
+ </li>
129
+ }
130
+ </ul>
131
+ <h3>Editable bindings</h3>
132
+ <ul class="studio__settings">
133
+ @for (b of s.bindings; track b.scope_id + '/' + b.resource_kind + '/' + b.resource_id) {
134
+ @if (b.editable) {
135
+ <li>
136
+ <strong>{{ b.resource_kind }} / {{ b.resource_id }}</strong> at {{ b.scope_kind }}
137
+ ({{ b.merge_mode | sailCaption:'config_merge_mode' }}@if (b.sealed) {, sealed})
138
+ <mat-form-field>
139
+ <mat-label>Value (JSON)</mat-label>
140
+ <textarea matInput rows="4" #value [value]="b.value | json" [disabled]="!can('EDIT')"></textarea>
141
+ </mat-form-field>
142
+ @if (can('EDIT')) {
143
+ <button matButton type="button" [disabled]="busy()" (click)="saveSetting(b, value.value)">Save</button>
144
+ <button matButton type="button" [disabled]="busy()" (click)="saveSetting(b, '', true)">Remove</button>
145
+ }
146
+ </li>
147
+ }
148
+ }
149
+ </ul>
150
+ @if (can('EDIT')) {
151
+ <fieldset class="studio__add-setting">
152
+ <legend>Add a binding</legend>
153
+ <mat-form-field>
154
+ <mat-label>Scope</mat-label>
155
+ <mat-select #scope [value]="s.agent_scope_id">
156
+ <mat-option [value]="s.agent_scope_id">This agent</mat-option>
157
+ <mat-option [value]="s.type_scope_id">Every {{ d.agent_type }} agent</mat-option>
158
+ </mat-select>
159
+ </mat-form-field>
160
+ <mat-form-field><mat-label>Resource kind</mat-label><input matInput #kind></mat-form-field>
161
+ <mat-form-field><mat-label>Resource id</mat-label><input matInput #rid></mat-form-field>
162
+ <mat-form-field>
163
+ <mat-label>Merge mode</mat-label>
164
+ <mat-select #merge value="R">
165
+ @for (mode of mergeModes(); track mode.code) { <mat-option [value]="mode.code">{{ mode.caption }}</mat-option> }
166
+ </mat-select>
167
+ </mat-form-field>
168
+ <mat-form-field><mat-label>Value (JSON)</mat-label><textarea matInput rows="3" #val></textarea></mat-form-field>
169
+ <button matButton type="button" [disabled]="busy()"
170
+ (click)="addSetting(scope.value, kind.value, rid.value, merge.value, val.value)">Add</button>
171
+ </fieldset>
172
+ }
173
+ }
174
+ }
175
+ @case ('test') {
176
+ @if (can('TEST')) {
177
+ @if (dirty()) { <p role="status">Unsaved changes are not tested; save the draft first.</p> }
178
+ <mat-form-field><mat-label>Task (optional)</mat-label><input matInput #task></mat-form-field>
179
+ <mat-form-field><mat-label>Sample input (optional)</mat-label><textarea matInput rows="3" #sample></textarea></mat-form-field>
180
+ <button matButton="elevated" type="button" [disabled]="busy()" (click)="runTest(task.value, sample.value)">Run test</button>
181
+ @if (testResult(); as r) {
182
+ <div class="studio__test-result" aria-live="polite">
183
+ <p>{{ r.model }} · {{ r.latency_ms }} ms · {{ r.input_tokens }} in / {{ r.output_tokens }} out · cost {{ r.credits }} · config {{ r.digest.slice(0, 12) }}</p>
184
+ <p>Sections: {{ r.sections.join(', ') }}</p>
185
+ <pre>{{ r.output }}</pre>
186
+ </div>
187
+ }
188
+ } @else {
189
+ <p>You are not authorized to run tests.</p>
190
+ }
191
+ }
192
+ @case ('history') {
193
+ @if (diff(); as df) {
194
+ <section class="studio__diff" aria-label="Version comparison">
195
+ <h3>v{{ df.previous }} → v{{ df.version }}</h3>
196
+ <button matButton type="button" (click)="diff.set(null)">Close</button>
197
+ @for (row of df.rows; track row.language_code + '#' + row.prompt_header_id) {
198
+ <div [class]="'studio__diff-row studio__diff-row--' + row.status">
199
+ <span>{{ row.caption }} · {{ row.language_code }} · {{ row.status }}</span>
200
+ @if (row.old_text) { <pre class="studio__diff-old">{{ row.old_text }}</pre> }
201
+ @if (row.new_text) { <pre class="studio__diff-new">{{ row.new_text }}</pre> }
202
+ </div>
203
+ } @empty {
204
+ <p>Prompt sections are identical; the versions differ only in configuration.</p>
205
+ }
206
+ </section>
207
+ }
208
+ <ul class="studio__releases">
209
+ @for (release of releases(); track release.version) {
210
+ <li>
211
+ <strong>v{{ release.version }}</strong> @if (release.active) { <span>Active</span> }
212
+ {{ release.change_summary || 'No change summary' }}
213
+ · {{ release.published_at | date: 'medium' }} · {{ release.languages.join(', ') }}
214
+ @if (release.activation; as a) { · scheduled {{ a.effective_at | date: 'medium' }} ({{ a.state | sailCaption:'release_activation_state' }}) }
215
+ @if (previousVersion(release.version) > 0) {
216
+ <button matButton type="button" [disabled]="busy()" (click)="compare(release.version)">Diff</button>
217
+ }
218
+ @if (!release.active && can('RESTORE')) {
219
+ <button matButton type="button" [disabled]="busy()" (click)="restore(release.version)">Restore</button>
220
+ }
221
+ </li>
222
+ } @empty {
223
+ <li>Nothing published yet.</li>
224
+ }
225
+ </ul>
226
+ @if (audit().length > 0) {
227
+ <h3>Audit</h3>
228
+ <ul class="studio__audit">
229
+ @for (event of audit(); track $index) {
230
+ <li>{{ event.event }} · {{ event.detail }} · {{ event.event_time | date: 'medium' }}</li>
231
+ }
232
+ </ul>
233
+ }
234
+ }
235
+ @case ('guardrails') {
236
+ @if (guardrails(); as g) {
237
+ @if (g.problem) { <p class="studio__error" role="alert">{{ g.problem }}</p> }
238
+ @for (layer of g.inherited; track layer.layer) {
239
+ <h3>Inherited · layer {{ layer.layer }}@if (layer.digest) { · {{ layer.digest.slice(0, 12) }} }</h3>
240
+ <pre class="studio__rules">{{ layer.rules | json }}</pre>
241
+ }
242
+ <h3>This agent's rules</h3>
243
+ <p>Rules only add to the inherited ones or tighten a rule of the same id.</p>
244
+ <mat-form-field class="studio__rules-editor">
245
+ <mat-label>Rules (JSON array)</mat-label>
246
+ <textarea matInput rows="10" #rules [value]="g.rules | json" [disabled]="!can('EDIT', undefined, 'guardrails')"></textarea>
247
+ </mat-form-field>
248
+ @if (can('EDIT', undefined, 'guardrails')) {
249
+ <button matButton="elevated" type="button" [disabled]="busy()" (click)="saveGuardrails(rules.value)">Save rules</button>
250
+ }
251
+ <h3>Safety events</h3>
252
+ <ul class="studio__safety-events">
253
+ @for (event of safetyEvents(); track event.id) {
254
+ <li>
255
+ {{ event.occurred_at | date: 'medium' }} · {{ event.stage }} · layer {{ event.layer }} · {{ event.action }} ({{ event.severity }})
256
+ · rules {{ event.rule_ids.join(', ') }}
257
+ @if (event.version) { · v{{ event.version }} }
258
+ @if (event.actor_kind) { · {{ event.actor_kind }} {{ event.actor_id }} }
259
+ </li>
260
+ } @empty {
261
+ <li>No safety event recorded.</li>
262
+ }
263
+ </ul>
264
+ @if (!safetyExhausted()) {
265
+ <button matButton type="button" [disabled]="busy()" (click)="loadSafetyEvents()">Load more</button>
266
+ }
267
+ }
268
+ }
269
+ @default {
270
+ @if (productTab(); as t) {
271
+ @if (selected(); as agent) {
272
+ <ng-container [ngTemplateOutlet]="t.template" [ngTemplateOutletContext]="{ $implicit: agent }" />
273
+ }
274
+ }
275
+ }
276
+ }
277
+ </div>
278
+
279
+ @if (successMessage()) { <p class="studio__success" role="status">{{ successMessage() }}</p> }
280
+ @if (errorMessage()) { <p class="studio__error" role="alert">{{ errorMessage() }}</p> }
281
+
282
+ <footer class="studio__footer" [class.dirty]="dirty()">
283
+ @if (dirty()) {
284
+ <span>Unsaved changes</span>
285
+ <button matButton type="button" [disabled]="busy()" (click)="discard()">Discard</button>
286
+ }
287
+ @if (can('EDIT')) {
288
+ <button matButton="elevated" type="button" [disabled]="!dirty() || !promptsValid() || busy()" (click)="save()">Save draft</button>
289
+ }
290
+ @if (can('PUBLISH')) {
291
+ <mat-form-field>
292
+ <mat-label>Change summary</mat-label>
293
+ <input matInput [value]="changeSummary()" (input)="changeSummary.set($any($event.target).value)">
294
+ </mat-form-field>
295
+ <button matButton="elevated" type="button" [disabled]="dirty() || busy()" (click)="publish()">Publish</button>
296
+ }
297
+ </footer>
298
+ } @else {
299
+ @if (errorMessage()) { <p class="studio__error" role="alert">{{ errorMessage() }}</p> }
300
+ @if (!busy() && summaries().length === 0) { <p>No agents yet.</p> }
301
+ }
302
+ </main>
303
+ </div>