@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,61 @@
1
+ import { AgentFieldError, AgentValidationProblem } from '../model/agent_studio';
2
+
3
+ /**
4
+ * Why a Studio mutation answered 409. The backend reports a stale agent revision and a stale
5
+ * type-defaults revision with the same message, so both are `revision`: reload the draft.
6
+ */
7
+ export type StudioConflictCause = 'revision' | 'sealed' | 'other';
8
+
9
+ type ErrorShape = {
10
+ status?: unknown;
11
+ error?: unknown;
12
+ problem?: unknown;
13
+ headers?: { get?: (name: string) => string | null };
14
+ response?: { headers?: { get?: (name: string) => string | null } };
15
+ };
16
+
17
+ function shapeOf(err: unknown): ErrorShape | undefined {
18
+ return err && typeof err === 'object' ? (err as ErrorShape) : undefined;
19
+ }
20
+
21
+ function detailOf(err: unknown): string {
22
+ const shape = shapeOf(err);
23
+ const body = shape?.problem ?? shape?.error;
24
+ if (typeof body === 'string') return body;
25
+ if (!body || typeof body !== 'object') return '';
26
+ const { detail, message } = body as { detail?: unknown; message?: unknown };
27
+ if (typeof detail === 'string') return detail;
28
+ return typeof message === 'string' ? message : '';
29
+ }
30
+
31
+ export function studioConflictCause(err: unknown): StudioConflictCause | undefined {
32
+ if (shapeOf(err)?.status !== 409) return undefined;
33
+ const detail = detailOf(err);
34
+ if (detail.includes('binding sealed')) return 'sealed';
35
+ if (detail.includes('revision conflict')) return 'revision';
36
+ return 'other';
37
+ }
38
+
39
+ /** Field errors of a 400; the backend carries them as a JSON document in the problem detail. */
40
+ export function studioFieldErrors(err: unknown): AgentFieldError[] {
41
+ if (shapeOf(err)?.status !== 400) return [];
42
+ const detail = detailOf(err);
43
+ if (!detail.startsWith('{')) return [];
44
+ try {
45
+ const problem = JSON.parse(detail) as Partial<AgentValidationProblem>;
46
+ return Array.isArray(problem.fields) ? problem.fields : [];
47
+ } catch {
48
+ return [];
49
+ }
50
+ }
51
+
52
+ /** Whole seconds from `Retry-After` on a 429/503; undefined when the server gave no advice. */
53
+ export function retryAfterSeconds(err: unknown): number | undefined {
54
+ const shape = shapeOf(err);
55
+ if (shape?.status !== 429 && shape?.status !== 503) return undefined;
56
+ const headers = shape.headers ?? shape.response?.headers;
57
+ const raw = headers?.get?.('Retry-After');
58
+ if (!raw) return undefined;
59
+ const seconds = Number(raw);
60
+ return Number.isFinite(seconds) && seconds >= 0 ? seconds : undefined;
61
+ }
@@ -0,0 +1,115 @@
1
+ import {
2
+ EvidenceRef,
3
+ TurnApprovalEvent,
4
+ TurnEffectEvent,
5
+ TurnEvent,
6
+ TurnExtensionEvent,
7
+ TurnProgressEvent,
8
+ TurnToolEvent,
9
+ isKnownTurnEvent,
10
+ } from '../model/turn_event';
11
+
12
+ export type ToolCallApproval = 'pending' | 'resolved';
13
+
14
+ export interface ToolCallView {
15
+ kind: 'tool';
16
+ call_id: string;
17
+ tool_id: string;
18
+ tool_version: string;
19
+ arguments?: unknown;
20
+ result?: TurnToolEvent;
21
+ /** Set only for an `is_error` result; its output is never shown. */
22
+ errorClass?: string;
23
+ effect?: TurnEffectEvent;
24
+ approval?: ToolCallApproval;
25
+ decision?: string;
26
+ }
27
+
28
+ export type TurnTimelineItem =
29
+ | { kind: 'text'; text: string }
30
+ | ToolCallView
31
+ | { kind: 'evidence'; refs: EvidenceRef[] }
32
+ | { kind: 'progress'; progress: TurnProgressEvent }
33
+ | { kind: 'extension'; extension: TurnExtensionEvent }
34
+ | { kind: 'result' };
35
+
36
+ function errorClassOf(tool: TurnToolEvent): string | undefined {
37
+ if (!tool.is_error) return undefined;
38
+ const output = tool.output;
39
+ if (!output || typeof output !== 'object') return '';
40
+ const errorClass = (output as { error?: unknown }).error;
41
+ return typeof errorClass === 'string' ? errorClass : '';
42
+ }
43
+
44
+ /**
45
+ * Folds a turn's events, in frame order, into renderable items: text deltas concatenate, and a
46
+ * call's proposal, result, effect, and approval meet on one item by `call_id`. Unknown kinds
47
+ * are skipped.
48
+ */
49
+ export function buildTurnTimeline(events: readonly unknown[]): TurnTimelineItem[] {
50
+ const items: TurnTimelineItem[] = [];
51
+ const calls = new Map<string, ToolCallView>();
52
+
53
+ const callFor = (source: { call_id?: string; tool_id?: string; tool_version?: string }): ToolCallView => {
54
+ const callId = source.call_id ?? '';
55
+ let call = calls.get(callId);
56
+ if (!call) {
57
+ call = { kind: 'tool', call_id: callId, tool_id: source.tool_id ?? '', tool_version: source.tool_version ?? '' };
58
+ calls.set(callId, call);
59
+ items.push(call);
60
+ }
61
+ return call;
62
+ };
63
+
64
+ const applyApproval = (approval: TurnApprovalEvent | undefined, state: ToolCallApproval): void => {
65
+ if (!approval) return;
66
+ const call = callFor(approval);
67
+ call.approval = state;
68
+ call.decision = approval.decision;
69
+ };
70
+
71
+ for (const candidate of events) {
72
+ if (!isKnownTurnEvent(candidate)) continue;
73
+ const event: TurnEvent = candidate;
74
+ switch (event.kind) {
75
+ case 'text_delta': {
76
+ const last = items[items.length - 1];
77
+ if (last?.kind === 'text') last.text += event.text ?? '';
78
+ else items.push({ kind: 'text', text: event.text ?? '' });
79
+ break;
80
+ }
81
+ case 'tool_proposal':
82
+ if (event.tool) callFor(event.tool).arguments = event.tool.arguments;
83
+ break;
84
+ case 'tool_result':
85
+ if (event.tool) {
86
+ const call = callFor(event.tool);
87
+ call.result = event.tool;
88
+ call.errorClass = errorClassOf(event.tool);
89
+ }
90
+ break;
91
+ case 'effect':
92
+ if (event.effect) callFor(event.effect).effect = event.effect;
93
+ break;
94
+ case 'approval_pending':
95
+ applyApproval(event.approval, 'pending');
96
+ break;
97
+ case 'approval_resolved':
98
+ applyApproval(event.approval, 'resolved');
99
+ break;
100
+ case 'evidence':
101
+ if (event.evidence?.length) items.push({ kind: 'evidence', refs: event.evidence });
102
+ break;
103
+ case 'progress':
104
+ if (event.progress) items.push({ kind: 'progress', progress: event.progress });
105
+ break;
106
+ case 'extension':
107
+ if (event.extension) items.push({ kind: 'extension', extension: event.extension });
108
+ break;
109
+ case 'result':
110
+ items.push({ kind: 'result' });
111
+ break;
112
+ }
113
+ }
114
+ return items;
115
+ }