@otto-code/protocol 0.6.7 → 0.7.1
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/dist/agent-labels.d.ts +21 -0
- package/dist/agent-labels.js +66 -0
- package/dist/agent-types.d.ts +55 -0
- package/dist/agent-types.js +14 -0
- package/dist/generated/validation/ws-outbound.aot.js +38262 -31722
- package/dist/git-hosting.d.ts +2 -0
- package/dist/git-hosting.js +6 -0
- package/dist/messages.d.ts +7196 -430
- package/dist/messages.js +1872 -37
- package/dist/model-tiers.js +2 -0
- package/dist/orchestration.d.ts +172 -0
- package/dist/orchestration.js +226 -0
- package/dist/provider-config.d.ts +3 -1
- package/dist/provider-config.js +3 -0
- package/dist/validation/ws-outbound-schema-metadata.d.ts +1085 -15
- package/dist/widgets/bridge.d.ts +59 -0
- package/dist/widgets/bridge.js +87 -0
- package/dist/widgets/document.d.ts +18 -0
- package/dist/widgets/document.js +261 -0
- package/dist/widgets/icons.d.ts +20 -0
- package/dist/widgets/icons.js +90 -0
- package/dist/widgets/theme.d.ts +37 -0
- package/dist/widgets/theme.js +149 -0
- package/dist/widgets/types.d.ts +78 -0
- package/dist/widgets/types.js +88 -0
- package/package.json +1 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CSS custom properties a widget fragment is written against, and the
|
|
3
|
+
* mapping from Otto's semantic theme tokens onto them.
|
|
4
|
+
*
|
|
5
|
+
* The HOST assembles the guest document, not the daemon — because only the
|
|
6
|
+
* client knows which theme is live, and a theme switch has to re-skin an
|
|
7
|
+
* already-rendered widget without a daemon round trip. The variable NAMES are
|
|
8
|
+
* frozen here so the contract document, the fixtures, and every renderer agree.
|
|
9
|
+
*/
|
|
10
|
+
const FALLBACK_WARNING = "#d97706";
|
|
11
|
+
const FALLBACK_SERIF = "Georgia, 'Times New Roman', serif";
|
|
12
|
+
/**
|
|
13
|
+
* Parse `#rgb` / `#rrggbb` / `#rrggbbaa`. Alpha is dropped: these tints are
|
|
14
|
+
* composited against a known surface here, so carrying alpha through would
|
|
15
|
+
* double-apply it. Returns null for anything else (named colors, rgb(), the
|
|
16
|
+
* `rgba(0,0,0,0.06)` hover token) and callers pass such values through
|
|
17
|
+
* untouched rather than guessing.
|
|
18
|
+
*/
|
|
19
|
+
function parseHex(value) {
|
|
20
|
+
const hex = value.trim();
|
|
21
|
+
if (!hex.startsWith("#")) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const body = hex.slice(1);
|
|
25
|
+
if (body.length === 3) {
|
|
26
|
+
const r = Number.parseInt(body[0] + body[0], 16);
|
|
27
|
+
const g = Number.parseInt(body[1] + body[1], 16);
|
|
28
|
+
const b = Number.parseInt(body[2] + body[2], 16);
|
|
29
|
+
return Number.isNaN(r + g + b) ? null : { r, g, b };
|
|
30
|
+
}
|
|
31
|
+
if (body.length === 6 || body.length === 8) {
|
|
32
|
+
const r = Number.parseInt(body.slice(0, 2), 16);
|
|
33
|
+
const g = Number.parseInt(body.slice(2, 4), 16);
|
|
34
|
+
const b = Number.parseInt(body.slice(4, 6), 16);
|
|
35
|
+
return Number.isNaN(r + g + b) ? null : { r, g, b };
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function toHex({ r, g, b }) {
|
|
40
|
+
const channel = (value) => Math.max(0, Math.min(255, Math.round(value)))
|
|
41
|
+
.toString(16)
|
|
42
|
+
.padStart(2, "0");
|
|
43
|
+
return `#${channel(r)}${channel(g)}${channel(b)}`;
|
|
44
|
+
}
|
|
45
|
+
/** Composite `color` over `base` at `amount` opacity, opaquely. */
|
|
46
|
+
function mix(color, base, amount) {
|
|
47
|
+
const from = parseHex(color);
|
|
48
|
+
const to = parseHex(base);
|
|
49
|
+
if (!from || !to) {
|
|
50
|
+
return color;
|
|
51
|
+
}
|
|
52
|
+
return toHex({
|
|
53
|
+
r: to.r + (from.r - to.r) * amount,
|
|
54
|
+
g: to.g + (from.g - to.g) * amount,
|
|
55
|
+
b: to.b + (from.b - to.b) * amount,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A role color legible as TEXT on the widget background. The raw accent/danger
|
|
60
|
+
* tokens are tuned to sit on Otto's chrome, which is not always the same
|
|
61
|
+
* contrast problem as body copy on `--surface-0`, so nudge toward the
|
|
62
|
+
* foreground on dark themes and away from it on light ones.
|
|
63
|
+
*/
|
|
64
|
+
function roleText(color, input) {
|
|
65
|
+
return input.isDark ? mix(color, "#ffffff", 0.78) : mix(color, "#000000", 0.85);
|
|
66
|
+
}
|
|
67
|
+
/** A wash of a role color usable as a fill behind role text. */
|
|
68
|
+
function roleTint(color, input) {
|
|
69
|
+
return mix(color, input.surface0, input.isDark ? 0.22 : 0.12);
|
|
70
|
+
}
|
|
71
|
+
/** SVG palette. Fixed hues rather than theme tokens — a chart needs categorical
|
|
72
|
+
* separation that survives every theme, and the two dark/light rows keep each
|
|
73
|
+
* hue legible on whichever surface it lands on. */
|
|
74
|
+
const SVG_PALETTE_LIGHT = {
|
|
75
|
+
blue: "#2563eb",
|
|
76
|
+
teal: "#0d9488",
|
|
77
|
+
amber: "#b45309",
|
|
78
|
+
red: "#dc2626",
|
|
79
|
+
green: "#16a34a",
|
|
80
|
+
purple: "#7c3aed",
|
|
81
|
+
pink: "#db2777",
|
|
82
|
+
gray: "#52525b",
|
|
83
|
+
};
|
|
84
|
+
const SVG_PALETTE_DARK = {
|
|
85
|
+
blue: "#60a5fa",
|
|
86
|
+
teal: "#2dd4bf",
|
|
87
|
+
amber: "#fbbf24",
|
|
88
|
+
red: "#f87171",
|
|
89
|
+
green: "#4ade80",
|
|
90
|
+
purple: "#c084fc",
|
|
91
|
+
pink: "#f472b6",
|
|
92
|
+
gray: "#a1a1aa",
|
|
93
|
+
};
|
|
94
|
+
export function widgetPaletteFor(isDark) {
|
|
95
|
+
return isDark ? SVG_PALETTE_DARK : SVG_PALETTE_LIGHT;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The `:root` custom-property block. Split out from the stylesheet so a theme
|
|
99
|
+
* change can be reasoned about (and tested) without the 200 lines of rules.
|
|
100
|
+
*/
|
|
101
|
+
export function buildWidgetThemeVariables(input) {
|
|
102
|
+
const warning = input.warning ?? FALLBACK_WARNING;
|
|
103
|
+
const serif = input.fontSerif ?? FALLBACK_SERIF;
|
|
104
|
+
const palette = widgetPaletteFor(input.isDark);
|
|
105
|
+
const paletteVars = Object.entries(palette)
|
|
106
|
+
.map(([name, value]) => ` --c-${name}: ${value};`)
|
|
107
|
+
.join("\n");
|
|
108
|
+
return [
|
|
109
|
+
":root {",
|
|
110
|
+
` --surface-0: ${input.surface0};`,
|
|
111
|
+
` --surface-1: ${input.surface1};`,
|
|
112
|
+
` --surface-2: ${input.surface2};`,
|
|
113
|
+
` --text-primary: ${input.foreground};`,
|
|
114
|
+
` --text-secondary: ${mix(input.foreground, input.surface0, 0.75)};`,
|
|
115
|
+
` --text-muted: ${input.foregroundMuted};`,
|
|
116
|
+
` --text-accent: ${roleText(input.accent, input)};`,
|
|
117
|
+
` --text-danger: ${roleText(input.danger, input)};`,
|
|
118
|
+
` --text-success: ${roleText(input.success, input)};`,
|
|
119
|
+
` --text-warning: ${roleText(warning, input)};`,
|
|
120
|
+
` --bg-accent: ${roleTint(input.accent, input)};`,
|
|
121
|
+
` --bg-danger: ${roleTint(input.danger, input)};`,
|
|
122
|
+
` --bg-success: ${roleTint(input.success, input)};`,
|
|
123
|
+
` --bg-warning: ${roleTint(warning, input)};`,
|
|
124
|
+
` --border: ${input.border};`,
|
|
125
|
+
` --border-strong: ${mix(input.foreground, input.border, 0.25)};`,
|
|
126
|
+
` --border-stronger: ${mix(input.foreground, input.border, 0.5)};`,
|
|
127
|
+
` --border-accent: ${mix(input.accent, input.border, 0.6)};`,
|
|
128
|
+
` --border-danger: ${mix(input.danger, input.border, 0.6)};`,
|
|
129
|
+
` --border-success: ${mix(input.success, input.border, 0.6)};`,
|
|
130
|
+
` --border-warning: ${mix(warning, input.border, 0.6)};`,
|
|
131
|
+
` --font-sans: ${input.fontSans};`,
|
|
132
|
+
` --font-mono: ${input.fontMono};`,
|
|
133
|
+
` --font-voice: ${serif};`,
|
|
134
|
+
" --radius: 8px;",
|
|
135
|
+
" --radius-sm: 4px;",
|
|
136
|
+
" --pad-sm: 8px;",
|
|
137
|
+
" --pad-md: 12px;",
|
|
138
|
+
" --pad-lg: 16px;",
|
|
139
|
+
" --pad-xl: 24px;",
|
|
140
|
+
" --gap-xs: 4px;",
|
|
141
|
+
" --gap-sm: 8px;",
|
|
142
|
+
" --gap-md: 12px;",
|
|
143
|
+
" --gap-lg: 16px;",
|
|
144
|
+
" --gap-xl: 24px;",
|
|
145
|
+
paletteVars,
|
|
146
|
+
"}",
|
|
147
|
+
].join("\n");
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=theme.js.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Inline widgets — an agent emits a tool call carrying a fragment of HTML or
|
|
4
|
+
* SVG, and Otto renders it inline in the transcript at that call's position.
|
|
5
|
+
*
|
|
6
|
+
* Widgets are NOT artifacts. An artifact is a file-backed document the user
|
|
7
|
+
* keeps (see `../artifacts/types.ts`); a widget is a thought the model had in
|
|
8
|
+
* the middle of a sentence. It has no file, no versioning, and no lifetime
|
|
9
|
+
* beyond its transcript position — the tool-call input IS the content, so a
|
|
10
|
+
* re-opened chat re-renders its widgets from the stored timeline for free.
|
|
11
|
+
*
|
|
12
|
+
* See `docs/widgets.md`.
|
|
13
|
+
*/
|
|
14
|
+
/** Otto tool that carries a widget fragment. */
|
|
15
|
+
export declare const WIDGET_TOOL_NAME = "show_widget";
|
|
16
|
+
/** Companion tool returning the host contract the model codes against. */
|
|
17
|
+
export declare const WIDGET_CONTRACT_TOOL_NAME = "widget_contract";
|
|
18
|
+
/**
|
|
19
|
+
* Key under a tool call's `metadata` where the normalized widget payload rides.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately NOT a new `ToolCallDetail` variant: `ToolCallDetailPayloadSchema`
|
|
22
|
+
* (messages.ts) is a `z.discriminatedUnion`, so a client that predates widgets
|
|
23
|
+
* would reject an unknown discriminator and fail to parse the ENTIRE timeline
|
|
24
|
+
* message, not just the widget. `metadata` is `z.record(z.string(), z.unknown())`
|
|
25
|
+
* — an old client carries this through untouched and renders the tool call's
|
|
26
|
+
* `plain_text` detail as an ordinary row. That is the protocol contract doing
|
|
27
|
+
* its job.
|
|
28
|
+
*/
|
|
29
|
+
export declare const WIDGET_METADATA_KEY = "ottoWidget";
|
|
30
|
+
/**
|
|
31
|
+
* Payload shape version. Bumped only on a breaking change to the fields below;
|
|
32
|
+
* a client that does not recognize the version renders the fallback row rather
|
|
33
|
+
* than guessing.
|
|
34
|
+
*/
|
|
35
|
+
export declare const WIDGET_PAYLOAD_VERSION = 1;
|
|
36
|
+
/**
|
|
37
|
+
* How the fragment is interpreted. Auto-detected from the code itself (a
|
|
38
|
+
* fragment starting with `<svg` is SVG), never asked of the model — one less
|
|
39
|
+
* thing for it to get wrong.
|
|
40
|
+
*/
|
|
41
|
+
export declare const WIDGET_MODES: readonly ["html", "svg"];
|
|
42
|
+
export type WidgetMode = (typeof WIDGET_MODES)[number];
|
|
43
|
+
/**
|
|
44
|
+
* Hard ceiling on a fragment. A widget rides in the timeline and is re-sent on
|
|
45
|
+
* every backfill, so an unbounded one is a permanent tax on the conversation.
|
|
46
|
+
* Generous enough for a dense dashboard; anything past it is a document, and a
|
|
47
|
+
* document is an artifact.
|
|
48
|
+
*/
|
|
49
|
+
export declare const WIDGET_MAX_CODE_CHARS = 128000;
|
|
50
|
+
/** Loading messages shown while the fragment streams in. */
|
|
51
|
+
export declare const WIDGET_MAX_LOADING_MESSAGES = 4;
|
|
52
|
+
export declare const WIDGET_MAX_LOADING_MESSAGE_CHARS = 120;
|
|
53
|
+
export declare const WIDGET_MAX_TITLE_CHARS = 120;
|
|
54
|
+
declare const WidgetPayloadSchema: z.ZodObject<{
|
|
55
|
+
version: z.ZodNumber;
|
|
56
|
+
id: z.ZodString;
|
|
57
|
+
title: z.ZodString;
|
|
58
|
+
mode: z.ZodEnum<{
|
|
59
|
+
html: "html";
|
|
60
|
+
svg: "svg";
|
|
61
|
+
}>;
|
|
62
|
+
code: z.ZodString;
|
|
63
|
+
loadingMessages: z.ZodArray<z.ZodString>;
|
|
64
|
+
truncated: z.ZodOptional<z.ZodBoolean>;
|
|
65
|
+
}, z.core.$strip>;
|
|
66
|
+
export type WidgetPayload = z.infer<typeof WidgetPayloadSchema>;
|
|
67
|
+
/**
|
|
68
|
+
* Read a widget payload out of a tool call's metadata bag.
|
|
69
|
+
*
|
|
70
|
+
* Returns null for anything that is not a widget of a version this build
|
|
71
|
+
* understands — the caller renders the ordinary tool-call row instead. Never
|
|
72
|
+
* throws: metadata is `unknown` by contract and may come from a newer daemon.
|
|
73
|
+
*/
|
|
74
|
+
export declare function readWidgetPayload(metadata: Record<string, unknown> | undefined): WidgetPayload | null;
|
|
75
|
+
/** Detect the render mode from the fragment itself. */
|
|
76
|
+
export declare function detectWidgetMode(code: string): WidgetMode;
|
|
77
|
+
export {};
|
|
78
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Inline widgets — an agent emits a tool call carrying a fragment of HTML or
|
|
4
|
+
* SVG, and Otto renders it inline in the transcript at that call's position.
|
|
5
|
+
*
|
|
6
|
+
* Widgets are NOT artifacts. An artifact is a file-backed document the user
|
|
7
|
+
* keeps (see `../artifacts/types.ts`); a widget is a thought the model had in
|
|
8
|
+
* the middle of a sentence. It has no file, no versioning, and no lifetime
|
|
9
|
+
* beyond its transcript position — the tool-call input IS the content, so a
|
|
10
|
+
* re-opened chat re-renders its widgets from the stored timeline for free.
|
|
11
|
+
*
|
|
12
|
+
* See `docs/widgets.md`.
|
|
13
|
+
*/
|
|
14
|
+
/** Otto tool that carries a widget fragment. */
|
|
15
|
+
export const WIDGET_TOOL_NAME = "show_widget";
|
|
16
|
+
/** Companion tool returning the host contract the model codes against. */
|
|
17
|
+
export const WIDGET_CONTRACT_TOOL_NAME = "widget_contract";
|
|
18
|
+
/**
|
|
19
|
+
* Key under a tool call's `metadata` where the normalized widget payload rides.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately NOT a new `ToolCallDetail` variant: `ToolCallDetailPayloadSchema`
|
|
22
|
+
* (messages.ts) is a `z.discriminatedUnion`, so a client that predates widgets
|
|
23
|
+
* would reject an unknown discriminator and fail to parse the ENTIRE timeline
|
|
24
|
+
* message, not just the widget. `metadata` is `z.record(z.string(), z.unknown())`
|
|
25
|
+
* — an old client carries this through untouched and renders the tool call's
|
|
26
|
+
* `plain_text` detail as an ordinary row. That is the protocol contract doing
|
|
27
|
+
* its job.
|
|
28
|
+
*/
|
|
29
|
+
export const WIDGET_METADATA_KEY = "ottoWidget";
|
|
30
|
+
/**
|
|
31
|
+
* Payload shape version. Bumped only on a breaking change to the fields below;
|
|
32
|
+
* a client that does not recognize the version renders the fallback row rather
|
|
33
|
+
* than guessing.
|
|
34
|
+
*/
|
|
35
|
+
export const WIDGET_PAYLOAD_VERSION = 1;
|
|
36
|
+
/**
|
|
37
|
+
* How the fragment is interpreted. Auto-detected from the code itself (a
|
|
38
|
+
* fragment starting with `<svg` is SVG), never asked of the model — one less
|
|
39
|
+
* thing for it to get wrong.
|
|
40
|
+
*/
|
|
41
|
+
export const WIDGET_MODES = ["html", "svg"];
|
|
42
|
+
/**
|
|
43
|
+
* Hard ceiling on a fragment. A widget rides in the timeline and is re-sent on
|
|
44
|
+
* every backfill, so an unbounded one is a permanent tax on the conversation.
|
|
45
|
+
* Generous enough for a dense dashboard; anything past it is a document, and a
|
|
46
|
+
* document is an artifact.
|
|
47
|
+
*/
|
|
48
|
+
export const WIDGET_MAX_CODE_CHARS = 128000;
|
|
49
|
+
/** Loading messages shown while the fragment streams in. */
|
|
50
|
+
export const WIDGET_MAX_LOADING_MESSAGES = 4;
|
|
51
|
+
export const WIDGET_MAX_LOADING_MESSAGE_CHARS = 120;
|
|
52
|
+
export const WIDGET_MAX_TITLE_CHARS = 120;
|
|
53
|
+
const WidgetPayloadSchema = z.object({
|
|
54
|
+
version: z.number().int().positive(),
|
|
55
|
+
id: z.string().min(1),
|
|
56
|
+
title: z.string(),
|
|
57
|
+
mode: z.enum(WIDGET_MODES),
|
|
58
|
+
code: z.string(),
|
|
59
|
+
loadingMessages: z.array(z.string()),
|
|
60
|
+
/** Set when the fragment hit {@link WIDGET_MAX_CODE_CHARS} and was cut. */
|
|
61
|
+
truncated: z.boolean().optional(),
|
|
62
|
+
});
|
|
63
|
+
/**
|
|
64
|
+
* Read a widget payload out of a tool call's metadata bag.
|
|
65
|
+
*
|
|
66
|
+
* Returns null for anything that is not a widget of a version this build
|
|
67
|
+
* understands — the caller renders the ordinary tool-call row instead. Never
|
|
68
|
+
* throws: metadata is `unknown` by contract and may come from a newer daemon.
|
|
69
|
+
*/
|
|
70
|
+
export function readWidgetPayload(metadata) {
|
|
71
|
+
const raw = metadata?.[WIDGET_METADATA_KEY];
|
|
72
|
+
if (raw === undefined || raw === null) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const parsed = WidgetPayloadSchema.safeParse(raw);
|
|
76
|
+
if (!parsed.success) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
if (parsed.data.version !== WIDGET_PAYLOAD_VERSION) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return parsed.data;
|
|
83
|
+
}
|
|
84
|
+
/** Detect the render mode from the fragment itself. */
|
|
85
|
+
export function detectWidgetMode(code) {
|
|
86
|
+
return /^\s*<svg[\s>]/i.test(code) ? "svg" : "html";
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=types.js.map
|