@contraagent/ui 0.2.0 → 0.4.0

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 (52) hide show
  1. package/custom-elements.json +93 -1
  2. package/dist/adapters/graph/project.d.ts.map +1 -1
  3. package/dist/adapters/graph/project.js +10 -3
  4. package/dist/adapters/graph/project.js.map +1 -1
  5. package/dist/adapters/journal-columns.d.ts.map +1 -1
  6. package/dist/adapters/journal-columns.js +7 -1
  7. package/dist/adapters/journal-columns.js.map +1 -1
  8. package/dist/adapters/trends.d.ts.map +1 -1
  9. package/dist/adapters/trends.js +26 -0
  10. package/dist/adapters/trends.js.map +1 -1
  11. package/dist/contraagent-ui.js +673 -663
  12. package/dist/elements/lists/ca-panel-body.d.ts +9 -1
  13. package/dist/elements/lists/ca-panel-body.d.ts.map +1 -1
  14. package/dist/elements/lists/ca-panel-body.js +34 -1
  15. package/dist/elements/lists/ca-panel-body.js.map +1 -1
  16. package/dist/elements/lists/ca-panel-view.d.ts +2 -0
  17. package/dist/elements/lists/ca-panel-view.d.ts.map +1 -1
  18. package/dist/elements/lists/ca-panel-view.js +6 -1
  19. package/dist/elements/lists/ca-panel-view.js.map +1 -1
  20. package/dist/elements/overview/ca-overview.d.ts +2 -0
  21. package/dist/elements/overview/ca-overview.d.ts.map +1 -1
  22. package/dist/elements/overview/ca-overview.js +34 -11
  23. package/dist/elements/overview/ca-overview.js.map +1 -1
  24. package/dist/elements/shell/views.js +1 -1
  25. package/dist/elements/shell/views.js.map +1 -1
  26. package/dist/elements/task/header.d.ts.map +1 -1
  27. package/dist/elements/task/header.js +17 -2
  28. package/dist/elements/task/header.js.map +1 -1
  29. package/dist/locales/ja.d.ts +11 -0
  30. package/dist/locales/ja.d.ts.map +1 -1
  31. package/dist/locales/ja.js +11 -0
  32. package/dist/locales/ja.js.map +1 -1
  33. package/dist/words/index.d.ts +1 -0
  34. package/dist/words/index.d.ts.map +1 -1
  35. package/dist/words/index.js +1 -0
  36. package/dist/words/index.js.map +1 -1
  37. package/dist/words/safety.d.ts +16 -0
  38. package/dist/words/safety.d.ts.map +1 -0
  39. package/dist/words/safety.js +26 -0
  40. package/dist/words/safety.js.map +1 -0
  41. package/package.json +4 -4
  42. package/src/adapters/graph/project.ts +10 -3
  43. package/src/adapters/journal-columns.ts +7 -1
  44. package/src/adapters/trends.ts +27 -0
  45. package/src/elements/lists/ca-panel-body.ts +30 -2
  46. package/src/elements/lists/ca-panel-view.ts +3 -1
  47. package/src/elements/overview/ca-overview.ts +36 -11
  48. package/src/elements/shell/views.ts +1 -1
  49. package/src/elements/task/header.ts +17 -2
  50. package/src/locales/ja.ts +11 -0
  51. package/src/words/index.ts +1 -0
  52. package/src/words/safety.ts +37 -0
@@ -1,4 +1,4 @@
1
- import { typedBody, type CodeBody, type PanelBody, type TableBody, type TreeBody, type TreeLink, type TypedPanelBody } from '@contraagent/api';
1
+ import { typedBody, type CodeBody, type EmbedBody, type PanelBody, type TableBody, type TreeBody, type TreeLink, type TypedPanelBody } from '@contraagent/api';
2
2
  import '@contraagent/elements/charts/define';
3
3
  import '@contraagent/elements/code/define';
4
4
  import '@contraagent/elements/data-table/define';
@@ -35,7 +35,9 @@ const CELLS = [
35
35
 
36
36
  /**
37
37
  * A pipeline's own view drawn from its typed body (`PanelBody`): tiles, a table, charts,
38
- * a status grid, notes, code, trees, and blocks of them one under another, each under its title
38
+ * a status grid, notes, code, trees, a page in a frame (an app the monitor serves, by a path
39
+ * relative to `base-url`, or an http(s) URL; given the theme shown as `theme`, `dark` or
40
+ * `light`), and blocks of them one under another, each under its title
39
41
  * (folded when the body says `collapsed`). A tree whose nodes hold code shows the selected
40
42
  * node's code beside it; a node with a link leads to a body of the panel (its `anchor`), a
41
43
  * run panel's tab or a task. A body of another shape, inside blocks, is shown as a JSON tree.
@@ -70,10 +72,16 @@ export class CaPanelBody extends LitElement {
70
72
  .pair > .tree { border: 1px solid var(--line); border-radius: var(--radius-md); max-height: 70vh; overflow: auto; }
71
73
  .pane { min-width: 0; }
72
74
  @media (max-width: 720px) { .pair { grid-template-columns: minmax(0, 1fr); } .pair > .tree { max-height: 40vh; } }
75
+ /* a page of the pipeline's own: as tall as the body says, else the screen under the monitor's header */
76
+ .embed { display: block; width: 100%; height: calc(100dvh - 160px); min-height: 320px; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface); }
73
77
  `,
74
78
  ];
75
79
 
76
80
  @property({ attribute: false }) body: PanelBody | undefined;
81
+ /** Where the monitor is served: an embedded page's path is relative to it. */
82
+ @property({ attribute: 'base-url' }) baseUrl = '';
83
+ /** The panel's title: the name of a framed page whose body has no title of its own. */
84
+ @property() label = '';
77
85
 
78
86
  /** The order of each table's rows, by the table's place in the body. */
79
87
  @state() private sorts = new Map<string, SortState>();
@@ -100,6 +108,15 @@ export class CaPanelBody extends LitElement {
100
108
  .path=${c.path ?? path ?? ''} .label=${c.title ?? c.path ?? path ?? ''} ?wrap=${!!c.wrap} .strings=${codeStrings()}></cae-code>`;
101
109
  }
102
110
 
111
+ /** A page in a frame: its URL against the monitor's, with the theme shown. */
112
+ private embed(b: EmbedBody): TemplateResult {
113
+ const base = new URL(this.baseUrl ? this.baseUrl.replace(/\/?$/, '/') : './', document.baseURI);
114
+ const url = new URL(b.url, base);
115
+ url.searchParams.set('theme', shownTheme(this));
116
+ return html`<iframe class="embed" src=${url.href} title=${b.title || this.label}
117
+ style=${b.height ? `height:${b.height}px` : nothing}></iframe>`;
118
+ }
119
+
103
120
  /** A tree, and the selected node's code beside it: the texts come with the body, so a selection sends no request. */
104
121
  private tree(b: TreeBody, at: string): TemplateResult {
105
122
  const { items, nodes, first } = bodyTree(b);
@@ -167,6 +184,8 @@ export class CaPanelBody extends LitElement {
167
184
  return this.code(b);
168
185
  case 'tree':
169
186
  return this.tree(b, at);
187
+ case 'embed':
188
+ return this.embed(b);
170
189
  default:
171
190
  return html`${nothing}`;
172
191
  }
@@ -193,6 +212,15 @@ export class CaPanelBody extends LitElement {
193
212
  }
194
213
  }
195
214
 
215
+ /** The theme `el` is shown in, `dark` or `light`: the nearest `data-ca-theme` above it, through shadow roots; else the screen's. */
216
+ function shownTheme(el: Element): 'dark' | 'light' {
217
+ for (let n: Node | null = el; n; n = n.parentNode ?? (n instanceof ShadowRoot ? n.host : null)) {
218
+ const theme = n instanceof Element ? n.getAttribute('data-ca-theme') : null;
219
+ if (theme) return theme.includes('light') ? 'light' : 'dark';
220
+ }
221
+ return matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
222
+ }
223
+
196
224
  declare global {
197
225
  interface HTMLElementTagNameMap {
198
226
  'ca-panel-body': CaPanelBody;
@@ -25,6 +25,8 @@ export class CaPanelView extends LitElement {
25
25
  @property({ attribute: false }) panel: PanelInfo | undefined;
26
26
  /** Its body; undefined until the feed has sent it. */
27
27
  @property({ attribute: false }) data: PanelBody | undefined;
28
+ /** Where the monitor is served: an embedded page's path is relative to it. */
29
+ @property({ attribute: 'base-url' }) baseUrl = '';
28
30
 
29
31
  override render() {
30
32
  if (this.data === undefined) {
@@ -38,7 +40,7 @@ export class CaPanelView extends LitElement {
38
40
  <h3><span class="tab-ic" aria-hidden="true">${info?.icon || '▤'}</span>${info?.title || this.name}
39
41
  <span class="r">${msg('Custom view')}</span></h3>
40
42
  ${info?.description ? html`<div class="meta desc">${info.description}</div>` : nothing}
41
- ${typedBody(this.data) ? html`<ca-panel-body .body=${this.data}></ca-panel-body>` : html`<cae-json-tree .value=${this.data}></cae-json-tree>`}
43
+ ${typedBody(this.data) ? html`<ca-panel-body .body=${this.data} base-url=${this.baseUrl} label=${info?.title || this.name}></ca-panel-body>` : html`<cae-json-tree .value=${this.data}></cae-json-tree>`}
42
44
  </section>`;
43
45
  }
44
46
  }
@@ -46,6 +46,12 @@ import './ca-workers.js';
46
46
 
47
47
  /** Failed outputs listed on the overview; the rest are behind a link to the outputs tab. */
48
48
  const OUTPUTS_SHOWN = 5;
49
+ /** The problems of each group shown (failed, not started, partly done); the rest are in the history. */
50
+ const PROBLEMS_SHOWN = 5;
51
+ /** The tasks done after a retry shown: they are done, so fewer. */
52
+ const RECOVERED_SHOWN = 3;
53
+ /** The later tasks a failure stopped, shown as chips beside it. */
54
+ const STOPPED_SHOWN = 4;
49
55
 
50
56
  const CT_ORDER = ['invalid', 'refused', 'failed', 'running', 'pending', 'valid'] as const satisfies readonly (keyof OutputTally)[];
51
57
 
@@ -147,7 +153,9 @@ export class CaOverview extends LitElement {
147
153
  .cu { display: grid; grid-template-columns: 90px 1fr 70px auto; gap: 8px; align-items: center; }
148
154
  .cu-bar { height: 5px; border-radius: 3px; background: var(--track); overflow: hidden; }
149
155
  .cu-bar i { display: block; height: 100%; background: var(--meter); }
150
- .fl > div.meta { grid-column: 1 / 4; overflow: hidden; text-overflow: ellipsis; }
156
+ /* one line each, the whole text in its tooltip: a long reason does not make the card taller */
157
+ .fl > div.meta { grid-column: 1 / 4; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
158
+ .ofl > .meta { white-space: nowrap; }
151
159
  .stopped { grid-column: 1 / 4; color: var(--text-muted); font-size: 11px; display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
152
160
  .fl.muted { opacity: 0.7; }
153
161
  .problems h4.sub { margin: 10px 0 2px; font-size: 11px; font-weight: 600; color: var(--text-muted); display: flex; }
@@ -307,7 +315,7 @@ export class CaOverview extends LitElement {
307
315
  ${statusWord({ text: o.status ? contractStatusLabel(o.status) : '—', tone: contractTone(o.status), title: o.status ? contractStatusWhy(o.status) : null })}
308
316
  <b class="jmono" title=${c.key}>${kindUnit({ kind: c.kind, unit: c.unit === c.key ? '' : c.unit })}</b>
309
317
  <span class="meta">${formatOf(o)}</span>
310
- <span class="meta">${runRelative(o.errors?.[0] || o.path, this.run).slice(0, 160)}</span>
318
+ <span class="meta" title=${runRelative(o.errors?.[0] || o.path, this.run)}>${runRelative(o.errors?.[0] || o.path, this.run)}</span>
311
319
  </button>`,
312
320
  )}
313
321
  ${bad.length > shown.length
@@ -422,49 +430,60 @@ export class CaOverview extends LitElement {
422
430
  const issues = failed.length + notStarted.length + partial.length;
423
431
  const ok = html`<div class="ok"><span aria-hidden="true">✓</span>${msg('No problems')}</div>`;
424
432
  if (!issues && !recovered.length) return ok;
425
- const reason = (t: TaskOutcome) => runRelative(t.attempts[t.attempts.length - 1]?.reason || '—', this.run).slice(0, 200);
433
+ const reason = (t: TaskOutcome) => runRelative(t.attempts[t.attempts.length - 1]?.reason || '—', this.run);
426
434
  const stoppedLine = (stopped: TaskOutcome[]) =>
427
435
  stopped.length
428
436
  ? html`<div class="stopped">
429
437
  <span>${stopped.length === 1 ? msg('1 later task not started') : msg(str`${stopped.length} later tasks not started`)}</span>
430
- ${stopped.map((x) => taskChip(this, this.name(x), x))}
438
+ ${stopped.slice(0, STOPPED_SHOWN).map((x) => taskChip(this, this.name(x), x))}
439
+ ${stopped.length > STOPPED_SHOWN ? html`<span>${msg(str`${stopped.length - STOPPED_SHOWN} more`)}</span>` : nothing}
431
440
  </div>`
432
441
  : nothing;
442
+ // Each group shows its first few; a link leads to the rest, in the history filtered to them.
443
+ const rest = (all: number, shown: number, stop: string) =>
444
+ all > shown
445
+ ? html`<div class="more"><a class="lnk" href="#" @click=${(e: Event) => this.nav(e, () => emit(this, 'ca-navigate', { tab: 'journal', stop }))}>${all - shown === 1
446
+ ? msg('1 more in history')
447
+ : msg(str`${all - shown} more in history`)}</a></div>`
448
+ : nothing;
433
449
  // The tasks done after retrying are done, not problems: a group of their own, after the problems.
434
450
  return html`${issues ? nothing : ok}${repeat(
435
- failed,
451
+ failed.slice(0, PROBLEMS_SHOWN),
436
452
  (p) => p.task.key,
437
453
  (p) => {
438
454
  const last = p.task.attempts[p.task.attempts.length - 1];
439
455
  return html`<div class="fl" @click=${(e: MouseEvent) => this.openRow(e, p.task)}>
440
456
  ${statusWord({ text: outcomeLabel('failed'), tone: 'failed' })}
441
457
  ${this.nameButton(p.task)}<span class="meta">${localTime(last?.ts, true)}</span>
442
- <div class="meta">${stopLabel(last?.stop)}: ${reason(p.task)}</div>
458
+ <div class="meta" title=${reason(p.task)}>${stopLabel(last?.stop)}: ${reason(p.task)}</div>
443
459
  ${stoppedLine(p.stopped)}
444
460
  </div>`;
445
461
  },
446
462
  )}
463
+ ${rest(failed.length, PROBLEMS_SHOWN, 'bad')}
447
464
  ${repeat(
448
- notStarted,
465
+ notStarted.slice(0, PROBLEMS_SHOWN),
449
466
  (t) => t.key,
450
467
  (t) => html`<div class="fl" @click=${(e: MouseEvent) => this.openRow(e, t)}>
451
468
  ${statusWord({ text: outcomeLabel('not_started'), tone: 'attention' })}
452
469
  ${this.nameButton(t)}<span></span>
453
- <div class="meta">${notStartedCause(t, outcomes, this.run) ? sentence(notStartedCause(t, outcomes, this.run)) : reason(t)}</div>
470
+ ${this.metaLine(notStartedCause(t, outcomes, this.run) ? sentence(notStartedCause(t, outcomes, this.run)) : reason(t))}
454
471
  </div>`,
455
472
  )}
473
+ ${rest(notStarted.length, PROBLEMS_SHOWN, 'invalid_input')}
456
474
  ${repeat(
457
- partial,
475
+ partial.slice(0, PROBLEMS_SHOWN),
458
476
  (t) => t.key,
459
477
  (t) => html`<div class="fl" @click=${(e: MouseEvent) => this.openRow(e, t)}>
460
478
  ${statusWord({ text: outcomeLabel('partial'), tone: outcomeTone('partial') })}
461
479
  ${this.nameButton(t)}<span></span>
462
- <div class="meta">${reason(t)}</div>
480
+ ${this.metaLine(reason(t))}
463
481
  </div>`,
464
482
  )}
483
+ ${rest(partial.length, PROBLEMS_SHOWN, 'partial')}
465
484
  ${recovered.length ? html`<h4 class="sub">${sentence(outcomeLabel('recovered'))} <span class="r">${recovered.length}</span></h4>` : nothing}
466
485
  ${repeat(
467
- recovered,
486
+ recovered.slice(0, RECOVERED_SHOWN),
468
487
  (t) => t.key,
469
488
  (t) => {
470
489
  const fails = t.tries.filter((a) => !a.ok).length;
@@ -478,9 +497,15 @@ export class CaOverview extends LitElement {
478
497
  </div>`;
479
498
  },
480
499
  )}
500
+ ${rest(recovered.length, RECOVERED_SHOWN, 'retried')}
481
501
  ${this.nextStep(live, issues)}`;
482
502
  }
483
503
 
504
+ /** A row's second line: one line, the whole of it in its tooltip. */
505
+ private metaLine(text: string) {
506
+ return html`<div class="meta" title=${text}>${text}</div>`;
507
+ }
508
+
484
509
  /** For a finished run with problems: how many tasks are not done (the tasks resume=True runs again). */
485
510
  private nextStep(live: boolean, problemCount: number) {
486
511
  if (live || !problemCount) return nothing;
@@ -61,7 +61,7 @@ export function tabView({ snap, viewRoute, derived, q, client, baseUrl }: TabVie
61
61
  default: {
62
62
  const name = viewRoute.tab.slice('panel:'.length);
63
63
  const info = snap.info.panels.find((p) => p.name === name);
64
- return html`<ca-panel-view .name=${name} .panel=${info} .data=${state.panels[name]}></ca-panel-view>`;
64
+ return html`<ca-panel-view .name=${name} .panel=${info} .data=${state.panels[name]} base-url=${baseUrl}></ca-panel-view>`;
65
65
  }
66
66
  }
67
67
  }
@@ -9,6 +9,7 @@ import { facts, fmtDur, kindUnit, listText, localTime, tokenCount, unreadEvents,
9
9
  import { attemptNames, deferredText, changedFiles, changedFilesTip, changeLabel, credentialLabel, credentialTip, locksLabel, locksTip, modelSwitch, outcomeLabel, priorityText, priorityTip, replayedLabel, replayedTip, stopLabel, type ChangeKind } from '../../words/index.js';
10
10
  import { contextWarnedText, contextWarnedTip, peakContextLine, peakContextTip, peakTry } from '../../words/context.js';
11
11
  import { readingShort, readingTip } from '../../words/quota.js';
12
+ import { safetyStopCount, safetyStopLine, safetyStopTip } from '../../words/safety.js';
12
13
  import { customColumnTip, customValues } from '../../words/row-extras.js';
13
14
  import { isStop, type Tone } from '../../lib/tone.js';
14
15
  import { executorIcon } from '../../words/executor.js';
@@ -159,6 +160,7 @@ export function taskHeader(c: TaskContext, key: string, onSearch: (query: string
159
160
  ${customLine(tries, d.row_extras ?? [])}
160
161
  ${searchFold(tries, onSearch)}
161
162
  ${changesFold(tries)}
163
+ ${safetyFold(tries)}
162
164
  ${unreadEvents(tries)
163
165
  ? html`<div class="meta unread" title=${msg('Event types this contraagent version does not know (unread_events)')}>
164
166
  <span aria-hidden="true">⚠</span> ${msg(str`Unrecognized events: ${unreadEvents(tries)}`)}
@@ -227,6 +229,18 @@ function changesFold(tries: JournalRow[]) {
227
229
  </details>`;
228
230
  }
229
231
 
232
+ /** The safety stops of the task's attempts, one per line (the attempt named when more than one has them); nothing when none had one. */
233
+ function safetyFold(tries: JournalRow[]) {
234
+ const names = attemptNames(tries);
235
+ const withStops = tries.filter((r) => r.safety_stops?.length).length;
236
+ if (!withStops) return nothing;
237
+ const lines = tries.flatMap((r, i) => (r.safety_stops ?? []).map((s) => facts(withStops > 1 ? (names[i] ?? '') : '', safetyStopLine(s))));
238
+ return html`<details class="io-fold">
239
+ <summary title=${safetyStopTip()}>${safetyStopCount(lines.length)}</summary>
240
+ <ul class="meta searches">${lines.map((l) => html`<li>${l}</li>`)}</ul>
241
+ </details>`;
242
+ }
243
+
230
244
  /** The latest attempt's custom columns (`row_extras`), name and value; each attempt's are in its chip's tooltip. */
231
245
  function customLine(tries: JournalRow[], extras: readonly string[]) {
232
246
  const last = tries[tries.length - 1];
@@ -269,12 +283,13 @@ function quotaLine(c: TaskContext, tries: JournalRow[]) {
269
283
  </div>`;
270
284
  }
271
285
 
272
- /** One finished attempt: how it ended, how long, its cost; the rest (when, corrections, model, model switch) in its tooltip. */
286
+ /** One finished attempt: how it ended, how long, its cost, its safety stops; the rest (when, corrections, model, model switch) in its tooltip. */
273
287
  function runChip(c: TaskContext, r: JournalRow, name: string, session: boolean, extras: readonly string[]) {
274
288
  const look = r.stop === 'partial' ? 'part' : r.ok ? 'ok' : 'bad';
275
289
  const played = !!c.replayMarks && !!r.replayed;
276
290
  const reason = runRelative(r.reason, c.run);
277
291
  const sw = modelSwitch(r.hops, null, r.skipped_hops);
292
+ const safety = r.safety_stops?.length ? safetyStopCount(r.safety_stops.length) : '';
278
293
  const tip = [
279
294
  msg(str`Ended ${localTime(r.ts, true)}`),
280
295
  correctionsOf(r),
@@ -291,6 +306,6 @@ function runChip(c: TaskContext, r: JournalRow, name: string, session: boolean,
291
306
  .join('\n');
292
307
  const cost = r.cost_usd !== null && r.cost_usd !== undefined ? usd(r.cost_usd) : '';
293
308
  return html`<span class="runchip ${look}" title=${tip}>
294
- ${mark(r)} ${facts(name, stopLabel(r.stop, r.reason), fmtDur(r.wall_s), cost, played ? replayedLabel() : '')}
309
+ ${mark(r)} ${facts(name, stopLabel(r.stop, r.reason), fmtDur(r.wall_s), cost, safety, played ? replayedLabel() : '')}
295
310
  </span>`;
296
311
  }
package/src/locales/ja.ts CHANGED
@@ -34,8 +34,10 @@
34
34
  's03b454fb08e876ad': `タブ`,
35
35
  's03ee3cfb0919c62c': `タスク`,
36
36
  's03fb84d070eb4d43': str`古い試行 ${0} 件は非表示`,
37
+ 's041489fa7f9e6fe6': str`ほか ${0} 件`,
37
38
  's04286bec7654cf3f': `タスクが宣言したパターンの下で、試行が作成・変更・削除したファイル(changes)`,
38
39
  's044f7ee7915b22a3': str`一部のみ完了 ${0} 件`,
40
+ 's04f1babead09ec6b': `停止した部分`,
39
41
  's051e95917479e3a4': `実行中のエージェントのセッション`,
40
42
  's05548eaadd7b3e53': `選択中の項目`,
41
43
  's061a40d8b1403d96': `OutputContract`,
@@ -119,6 +121,7 @@
119
121
  's1ebbae8f5810b65b': `入力`,
120
122
  's1f25e1e20469a9ea': `削除`,
121
123
  's1f53dce0721a5fdc': str`所要時間 ${0}`,
124
+ 's2043c2041d0a5142': `安全分類器による停止`,
122
125
  's20e04bdc808a6442': str`リセットまで ${0}`,
123
126
  's20f936012bd51f12': `依存先タスクが実行されなかった`,
124
127
  's20fd629bac8fddd1': `ホストなし`,
@@ -240,6 +243,7 @@
240
243
  's51383b9a7d305b0c': `入力・出力・キャッシュ読み込み・キャッシュ書き込みの合計`,
241
244
  's5144d023ec192e11': `実行が終了した`,
242
245
  's521a16a18cfbcf5a': `1 ユニット`,
246
+ 's5226f85458fa03d2': `安全分類器がエージェントのセッションの一部を止め、セッションはそのまま続いた(safety_stops)`,
243
247
  's525b03a82e5421df': `失敗の後の、次のモデルでの新しいエージェントのセッション(Router のホップ)`,
244
248
  's525c011dcf23c207': `記録から再生した試行(replayed)`,
245
249
  's529efbcb1a0cf166': str`上限 ${0}`,
@@ -502,6 +506,7 @@
502
506
  'sa434ad4d2100f245': `作成元`,
503
507
  'sa48318e1513f445c': `コマンドライン`,
504
508
  'sa4f4addc6a836fcf': str`最大 ${0} 回`,
509
+ 'sa508a8fb4e60520e': `安全分類器による停止 1 件`,
505
510
  'sa50a6326530d8a0d': `折りたたむ`,
506
511
  'sa57b5e86ecb7d735': `権限エラーになったツール呼び出し`,
507
512
  'sa58fd816ed43f98e': `周回数`,
@@ -585,6 +590,7 @@
585
590
  'sbd4aeb401e751129': `実行を切り替え(r)`,
586
591
  'sbd58f76b47025b9c': `停止の記録なし`,
587
592
  'sbd9a674a977ac3b7': `再開後`,
593
+ 'sbdc36a52f3bf5b79': str`履歴にほか ${0} 件`,
588
594
  'sbe299a9270c76655': `実行履歴の失敗した試行`,
589
595
  'sbe4c836868862bb2': str`${0} の記録から再生したエージェントのセッション(replay)`,
590
596
  'sbe920e285458a813': str`週(${0})`,
@@ -629,6 +635,7 @@
629
635
  'scbd3558d2cfc7dc3': `全文`,
630
636
  'sccad21b77f9a317c': str`最も高い種別: ${0}`,
631
637
  'sccce3de7b4abfdfe': `モデルが応答を拒否`,
638
+ 'scd16bc5ac18bdf91': `モデルの応答`,
632
639
  'scd27d0ff0f4f8757': str`${0}: ${1} → 次のモデル`,
633
640
  'scd945ec17b61f103': `エージェントの所要時間`,
634
641
  'scd9ce582cd9e0daf': str`優先度 ${0}`,
@@ -676,6 +683,7 @@
676
683
  'sdd4a8ac67f7885c1': `モデル 1 種類`,
677
684
  'sdd826d4b3e02f41b': `修正依頼なし`,
678
685
  'sddf65e6dc438188d': `ビューを表示`,
686
+ 'sde7cbc4291a82cc4': `履歴にほか 1 件`,
679
687
  'sde88e8c24887ecfe': str`アカウント ${0}`,
680
688
  'sdf14d39488c16f16': str`${0} 回目で完了 | 失敗 1 回 | 最初の失敗: ${1}`,
681
689
  'sdf452c95fdeb4be0': `リトライなし`,
@@ -701,6 +709,7 @@
701
709
  'se6de387a56331077': `下の階層あり`,
702
710
  'se7102b7d5942a59c': str`${0} 回目`,
703
711
  'se751edc73f4d0062': `リプレイと新しいセッション`,
712
+ 'se8d06d5375424a9c': str`安全分類器による停止 ${0} 件`,
704
713
  'se90f184c6aa3ffbb': `認証情報`,
705
714
  'se99de5c72fecbbc3': `ロック`,
706
715
  'se99f79da6128e386': str`終了(失敗したタスク ${0} 件)`,
@@ -753,11 +762,13 @@
753
762
  'sf7d87f0549046c0a': str`残り ${0}`,
754
763
  'sf7e6e6be222007c3': `すべての実行グループ`,
755
764
  'sf82a1121505029b9': str`休止 ${0} → ${1}`,
765
+ 'sf868eba1173ef02e': `内容不明`,
756
766
  'sf93c7009f950942a': `合計`,
757
767
  'sf9bbecb9ed98e966': `エージェントのセッションが始まらなかった`,
758
768
  'sfa04f4ef1995407e': `テキスト`,
759
769
  'sfa251e3af0f9c14c': str`${0} 〜 ${1}`,
760
770
  'sfa6a5b4597c720bc': `フィルターに一致するタスクなし`,
771
+ 'sfa82224480602f92': `ツールのリクエスト`,
761
772
  'sfadf0e9dfc4b3030': `修正依頼 1 回で完了`,
762
773
  'sfae8c6e1b5f558f4': `ホスト一覧`,
763
774
  'sfb33dc608ac218c4': `実行を切り替え`,
@@ -29,3 +29,4 @@ export * from './priority.js';
29
29
  export * from './context.js';
30
30
  export * from './preflight.js';
31
31
  export * from './task-definition.js';
32
+ export * from './safety.js';
@@ -0,0 +1,37 @@
1
+ // Words for safety stops: a safety classifier stopped part of an agent session, which
2
+ // went on and ended on its own outcome (`JournalRow.safety_stops`, `Trends.safety_stops`).
3
+ import type { JournalRow } from '@contraagent/api';
4
+ import { msg, str } from '@lit/localize';
5
+ import { facts, localTime } from './format.js';
6
+
7
+ /** One safety stop of an attempt. */
8
+ export type SafetyStop = NonNullable<JournalRow['safety_stops']>[number];
9
+
10
+ /** The heading of the safety stops (the trends' table, a count's column). */
11
+ export const safetyStopsLabel = (): string => msg('Safety stops');
12
+
13
+ /** A count of safety stops: `1 safety stop`, `3 safety stops`. */
14
+ export const safetyStopCount = (n: number): string => (n === 1 ? msg('1 safety stop') : msg(str`${n} safety stops`));
15
+
16
+ /** What a safety stop is, for its tooltip. */
17
+ export const safetyStopTip = (): string => msg('A safety classifier stopped part of the agent session, and the session went on (safety_stops)');
18
+
19
+ /** What the safety classifier stopped (`SafetyStop.part`), in words. */
20
+ export function safetyPartLabel(part: string | null | undefined): string {
21
+ switch (part) {
22
+ case 'response':
23
+ return msg('model response', { desc: 'A safety stop of a response of the model of the agent session' });
24
+ case 'tool':
25
+ return msg('tool request', { desc: 'A safety stop of a request of a model that a tool calls; the tool call ended in an error' });
26
+ case 'unread':
27
+ return msg('unrecognized', { desc: 'A safety stop the coding agent counted, in words this contraagent version does not recognize' });
28
+ default:
29
+ return part || '—';
30
+ }
31
+ }
32
+
33
+ /** The column of the trends' table that names what was stopped. */
34
+ export const safetyPartColumn = (): string => msg('Stopped part', { desc: 'A column: what a safety classifier stopped (a model response, a tool request)' });
35
+
36
+ /** One safety stop as a row of its known facts: `model response | opus | cyber | 2026-10-11 09:00:01`. */
37
+ export const safetyStopLine = (s: SafetyStop): string => facts(safetyPartLabel(s.part), s.model, s.category, s.tool, localTime(s.ts, true));