@manta-eu/horns-client 0.1.4 → 0.2.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.
@@ -0,0 +1,87 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { z } from "zod";
4
+ import { isPresent } from "@manta-eu/lib/present";
5
+ import { hornSchema, isRecord } from "@manta-eu/horns";
6
+ import { hornSnapshotSchema } from "@manta-eu/horns/render";
7
+ import { HornSnapshotView } from "@manta-eu/horns-kit";
8
+ import { writtenHornView } from "../components/written-horn-view";
9
+ import { PlanChanges } from "../components/plan-changes";
10
+ import { catalogCardView } from "../components/catalog-card-view";
11
+ import { ErrorState, Loading } from "../components/states";
12
+ import { problemFrom } from "../lib/problem";
13
+ import { appTheme } from "../lib/theme";
14
+ const aboutAnswer = z.object({
15
+ horn: hornSchema,
16
+ kept: z.boolean(),
17
+ slug: z.string().min(1).optional(),
18
+ revision: z.string().min(1).optional(),
19
+ });
20
+ /**
21
+ * Where the worker answers for a thing. The IRI goes in a query parameter and is encoded: a plan's carries
22
+ * a `#`, which would otherwise end the URL and ask about nothing.
23
+ */
24
+ export function hornAboutPath(datastoreId, about) {
25
+ return `/api/d/${encodeURIComponent(datastoreId)}/horns/by/about?iri=${encodeURIComponent(about)}`;
26
+ }
27
+ /** Where a Horn is turned into rows to draw. */
28
+ export function hornSnapshotPath(datastoreId) {
29
+ return `/api/d/${encodeURIComponent(datastoreId)}/horns/snapshot`;
30
+ }
31
+ /** Where every Horn about a thing is listed. */
32
+ export function hornsAboutPath(datastoreId, about, limit) {
33
+ const capped = limit === undefined ? "" : `&limit=${limit}`;
34
+ return `/api/d/${encodeURIComponent(datastoreId)}/horns/by/about/list?iri=${encodeURIComponent(about)}${capped}`;
35
+ }
36
+ export function HornDrawn(props) {
37
+ const { datastoreId, horn, cacheKey, variables = {}, mode = "embed", planActions, explain, onVariableChange } = props;
38
+ const snapshot = useQuery({
39
+ enabled: isPresent(horn),
40
+ queryKey: ["horn-snapshot", datastoreId, cacheKey, variables],
41
+ queryFn: async ({ signal }) => {
42
+ const response = await fetch(hornSnapshotPath(datastoreId), {
43
+ method: "POST",
44
+ headers: { "content-type": "application/json" },
45
+ body: JSON.stringify({ horn, variables }),
46
+ signal,
47
+ });
48
+ if (!response.ok)
49
+ throw new Error(await problemFrom(response, "The Horn could not be drawn."));
50
+ const body = await response.json();
51
+ return hornSnapshotSchema.parse(isRecord(body) ? body["snapshot"] : undefined);
52
+ },
53
+ });
54
+ if (snapshot.isError)
55
+ return _jsx(ErrorState, { title: "This could not be drawn", hint: snapshot.error.message });
56
+ if (!isPresent(snapshot.data))
57
+ return _jsx(Loading, { label: "Drawing this\u2026", rows: 3 });
58
+ return (_jsx(HornSnapshotView, { snapshot: snapshot.data, mode: mode, theme: appTheme(), writtenView: writtenHornView, planStrip: PlanChanges, catalogCard: catalogCardView, ...(isPresent(planActions) ? { planActions } : {}), ...(isPresent(explain) ? { explain } : {}), ...(isPresent(onVariableChange) ? { onVariableChange } : {}) }));
59
+ }
60
+ /**
61
+ * The Horn for a thing, drawn wherever a host puts it.
62
+ *
63
+ * A host with an IRI gets something to look at whether or not anyone has made a Horn for it: the worker
64
+ * answers with the kept one, or builds one from the plan or rule itself. So this needs no saved Horn, no
65
+ * route of the host's and no Studio — it is the read-only half, for a plan shown beside its own screen.
66
+ *
67
+ * Written views and catalog cards are bound here because they need `@manta-eu/react`, which the kit has not
68
+ * got. Everything else a host wants to hang on it is a prop.
69
+ */
70
+ export function HornAbout(props) {
71
+ const { datastoreId, about, variables = {}, mode = "embed", planActions, explain, onVariableChange } = props;
72
+ const seed = useQuery({
73
+ queryKey: ["horn-about", datastoreId, about],
74
+ queryFn: async ({ signal }) => {
75
+ const response = await fetch(hornAboutPath(datastoreId, about), { signal });
76
+ if (!response.ok)
77
+ throw new Error(await problemFrom(response, `The answer was ${response.status}.`));
78
+ return aboutAnswer.parse(await response.json());
79
+ },
80
+ });
81
+ if (seed.isError)
82
+ return _jsx(ErrorState, { title: "Nothing could be shown for this", hint: seed.error.message });
83
+ const horn = seed.data?.horn;
84
+ if (!isPresent(horn))
85
+ return _jsx(Loading, { label: "Drawing this\u2026", rows: 3 });
86
+ return (_jsx(HornDrawn, { datastoreId: datastoreId, horn: horn, cacheKey: seed.data?.revision ?? about, variables: variables, mode: mode, ...(isPresent(planActions) ? { planActions } : {}), ...(isPresent(explain) ? { explain } : {}), ...(isPresent(onVariableChange) ? { onVariableChange } : {}) }));
87
+ }
@@ -0,0 +1,29 @@
1
+ import { type HornDrawProps } from "./horn-about";
2
+ export type HornsAboutProps = HornDrawProps & {
3
+ datastoreId: string;
4
+ /** The thing these are about: a rule, a plan, a campaign, a person. Opaque — only ever passed on. */
5
+ about: string;
6
+ /** How many to draw. The worker caps it; leaving it off takes the worker's default. */
7
+ limit?: number;
8
+ variables?: Record<string, unknown>;
9
+ /**
10
+ * Where a Horn opens in full, where the host has somewhere to send a person. A plain href, not a route:
11
+ * the Horns UI may be another document, or another origin entirely.
12
+ */
13
+ hornHref?: (slug: string) => string;
14
+ /** What to say when nothing is about this thing yet. */
15
+ emptyHint?: string;
16
+ };
17
+ /**
18
+ * Every Horn about one thing, drawn.
19
+ *
20
+ * This is the whole of embedding Horns: a host that has an IRI — a rule, an action plan, a campaign, a
21
+ * person — puts this beside it and gets what Manta already knows about that thing, as pictures rather than
22
+ * links. It needs no saved Horn, no route of the host's and no Studio: the worker answers with the kept
23
+ * ones, or builds one from the rule or plan itself.
24
+ *
25
+ * The one thing a host must bring is the policy. A written view runs in the page's own realm, so the
26
+ * document has to carry `connect-src 'self'` and the rest of `contentSecurityPolicy()`; without it the
27
+ * views still draw, but nothing fences the code they came from.
28
+ */
29
+ export declare function HornsAbout(props: HornsAboutProps): import("react").JSX.Element;
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { z } from "zod";
4
+ import { isPresent } from "@manta-eu/lib/present";
5
+ import { hornSchema } from "@manta-eu/horns";
6
+ import { EmptyState, ErrorState, Loading } from "../components/states";
7
+ import { problemFrom } from "../lib/problem";
8
+ import { HornDrawn, hornsAboutPath } from "./horn-about";
9
+ const listAnswer = z.object({
10
+ horns: z.array(z.object({
11
+ horn: hornSchema,
12
+ kept: z.boolean(),
13
+ slug: z.string().min(1).optional(),
14
+ revision: z.string().min(1).optional(),
15
+ })),
16
+ total: z.number().int().min(0),
17
+ });
18
+ /**
19
+ * Every Horn about one thing, drawn.
20
+ *
21
+ * This is the whole of embedding Horns: a host that has an IRI — a rule, an action plan, a campaign, a
22
+ * person — puts this beside it and gets what Manta already knows about that thing, as pictures rather than
23
+ * links. It needs no saved Horn, no route of the host's and no Studio: the worker answers with the kept
24
+ * ones, or builds one from the rule or plan itself.
25
+ *
26
+ * The one thing a host must bring is the policy. A written view runs in the page's own realm, so the
27
+ * document has to carry `connect-src 'self'` and the rest of `contentSecurityPolicy()`; without it the
28
+ * views still draw, but nothing fences the code they came from.
29
+ */
30
+ export function HornsAbout(props) {
31
+ const { datastoreId, about, limit, variables = {}, hornHref, emptyHint, ...draw } = props;
32
+ const listed = useQuery({
33
+ queryKey: ["horns-about", datastoreId, about, limit ?? "default"],
34
+ queryFn: async ({ signal }) => {
35
+ const response = await fetch(hornsAboutPath(datastoreId, about, limit), { signal });
36
+ if (!response.ok)
37
+ throw new Error(await problemFrom(response, `The answer was ${response.status}.`));
38
+ return listAnswer.parse(await response.json());
39
+ },
40
+ });
41
+ if (listed.isError)
42
+ return _jsx(ErrorState, { title: "Nothing could be shown for this", hint: listed.error.message });
43
+ if (!isPresent(listed.data))
44
+ return _jsx(Loading, { label: "Looking for what is known about this\u2026", rows: 2 });
45
+ const { horns, total } = listed.data;
46
+ if (horns.length === 0) {
47
+ return (_jsx(EmptyState, { title: "Nothing is about this yet", hint: emptyHint ?? "Once somebody makes a Horn about it, it will be drawn here." }));
48
+ }
49
+ return (_jsxs("div", { "data-horns-about": true, className: "flex w-full flex-col gap-8", children: [horns.map(({ horn, slug, revision }) => (_jsxs("section", { "data-horn": slug, className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("h2", { className: "text-lg font-semibold text-default-foreground", children: isPresent(slug) && isPresent(hornHref) ? (_jsx("a", { href: hornHref(slug), className: "hover:underline", children: horn.title })) : (horn.title) }), isPresent(horn.summary) ? _jsx("p", { className: "text-muted-foreground text-sm", children: horn.summary }) : null] }), _jsx(HornDrawn, { datastoreId: datastoreId, horn: horn, cacheKey: revision ?? slug ?? about, variables: variables, ...draw })] }, slug ?? revision ?? horn.title))), total > horns.length ? (_jsxs("p", { className: "text-sm text-muted-foreground", children: [total - horns.length, " more ", total - horns.length === 1 ? "Horn is" : "Horns are", " about this."] })) : null] }));
50
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,9 @@
1
- export { AppRoutes, hornsRoutes } from "./app/routes.tsx";
1
+ export { AppRoutes, hornsRoutes, type HornsRoutesOptions } from "./app/routes.tsx";
2
+ export { hornsNavSections, type HornsNavItem, type HornsNavSection } from "./app/nav.ts";
2
3
  export { HornsApp } from "./standalone/app.tsx";
4
+ export { HornAbout, HornDrawn, hornAboutPath, hornsAboutPath, hornSnapshotPath } from "./embed/horn-about.tsx";
5
+ export type { HornAboutProps, HornDrawProps, HornDrawnProps } from "./embed/horn-about.tsx";
6
+ export { HornsAbout, type HornsAboutProps } from "./embed/horns-about.tsx";
3
7
  export { AppShell } from "./components/app-shell.tsx";
4
8
  export { signInHref } from "./lib/manta/sign-in.ts";
5
9
  export { useSparqlSelect } from "./lib/manta/sparql-select.ts";
package/dist/index.js CHANGED
@@ -7,9 +7,16 @@
7
7
  // What the browser needs beyond this is the stylesheet: `@manta-eu/horns-client/styles.css`, built here
8
8
  // rather than compiled by the host, so a host with no Tailwind still gets the app looking like Manta.
9
9
  export { AppRoutes, hornsRoutes } from "./app/routes.js";
10
+ // What a host with a shell of its own draws as navigation, so a page cannot be in one list and not the other.
11
+ export { hornsNavSections } from "./app/nav.js";
10
12
  // The session gate, and the only thing that provides the AuthContext every Horns page reads. A host
11
13
  // splicing `hornsRoutes()` into a router of its own mounts this above them, or provides that context itself.
12
14
  export { HornsApp } from "./standalone/app.js";
15
+ // A thing's Horn, drawn inside a host's own screen: it asks the worker for the kept Horn or one built
16
+ // from the plan or rule itself, so a host needs no saved Horn and no route of its own.
17
+ export { HornAbout, HornDrawn, hornAboutPath, hornsAboutPath, hornSnapshotPath } from "./embed/horn-about.js";
18
+ // Every Horn about one thing: what a host with an IRI embeds.
19
+ export { HornsAbout } from "./embed/horns-about.js";
13
20
  export { AppShell } from "./components/app-shell.js";
14
21
  export { signInHref } from "./lib/manta/sign-in.js";
15
22
  export { useSparqlSelect } from "./lib/manta/sparql-select.js";
@@ -0,0 +1,241 @@
1
+ import type { Horn } from "@manta-eu/horns";
2
+ import { type StudioStep } from "../studio-progress";
3
+ export type StudioConversationState = {
4
+ draft: Horn | null;
5
+ };
6
+ export type StudioTurn = {
7
+ reply: string;
8
+ horn: Horn | undefined;
9
+ };
10
+ /** One Studio conversation. The draft is the object's and arrives through state sync. */
11
+ export type StudioConversationArgs = {
12
+ datastoreId: string;
13
+ conversation: string;
14
+ /** Draws the Horn as it stands and says what is wrong with it. The page is the only thing that can. */
15
+ look: (why: string) => Promise<string>;
16
+ };
17
+ export declare function useStudioConversation(args: StudioConversationArgs): {
18
+ ask: (said: string) => Promise<StudioTurn>;
19
+ open: (horn: Horn) => Promise<void>;
20
+ steps: readonly StudioStep[];
21
+ draft: {
22
+ iri: string;
23
+ slug: string;
24
+ title: string;
25
+ datastoreId: string;
26
+ realm: "hiremanta-com" | "journey-travel";
27
+ schemaVersion: 1;
28
+ revision: number;
29
+ layout: {
30
+ kind: "grid";
31
+ columns: 12;
32
+ items: {
33
+ viewId: string;
34
+ x: number;
35
+ y: number;
36
+ w: number;
37
+ h: number;
38
+ }[];
39
+ };
40
+ variables: {
41
+ id: string;
42
+ label: string;
43
+ type: "string" | "number" | "date" | "iri" | "enum" | "dateRange";
44
+ default?: import("zod").JSONType | undefined;
45
+ options?: {
46
+ values: import("zod").JSONType[];
47
+ } | {
48
+ binding: {
49
+ kind: "select";
50
+ sparql: string;
51
+ params: string[];
52
+ limit: number;
53
+ derivations: string[];
54
+ columns: {
55
+ name: string;
56
+ type: "string" | "number" | "boolean" | "date" | "iri";
57
+ }[];
58
+ } | {
59
+ kind: "lens";
60
+ classIri: string;
61
+ limit: number;
62
+ derivations: string[];
63
+ fields: {
64
+ name: string;
65
+ path: string;
66
+ type: "string" | "number" | "boolean" | "date" | "iri";
67
+ }[];
68
+ rows?: "value" | "entity" | undefined;
69
+ } | {
70
+ kind: "query";
71
+ query: {
72
+ item: string;
73
+ revision: string;
74
+ hash: string;
75
+ };
76
+ params: string[];
77
+ limit: number;
78
+ };
79
+ } | undefined;
80
+ urlParam?: string | undefined;
81
+ }[];
82
+ views: ({
83
+ id: string;
84
+ title: string;
85
+ binding: {
86
+ kind: "select";
87
+ sparql: string;
88
+ params: string[];
89
+ limit: number;
90
+ derivations: string[];
91
+ columns: {
92
+ name: string;
93
+ type: "string" | "number" | "boolean" | "date" | "iri";
94
+ }[];
95
+ } | {
96
+ kind: "lens";
97
+ classIri: string;
98
+ limit: number;
99
+ derivations: string[];
100
+ fields: {
101
+ name: string;
102
+ path: string;
103
+ type: "string" | "number" | "boolean" | "date" | "iri";
104
+ }[];
105
+ rows?: "value" | "entity" | undefined;
106
+ } | {
107
+ kind: "query";
108
+ query: {
109
+ item: string;
110
+ revision: string;
111
+ hash: string;
112
+ };
113
+ params: string[];
114
+ limit: number;
115
+ };
116
+ render: {
117
+ kind: "component";
118
+ component: {
119
+ item: string;
120
+ revision: string;
121
+ hash: string;
122
+ };
123
+ props: import("zod").JSONType;
124
+ } | {
125
+ kind: "tsx";
126
+ source: string;
127
+ reviewed?: {
128
+ hash: string;
129
+ at: string;
130
+ } | undefined;
131
+ };
132
+ emits?: {
133
+ event: string;
134
+ setsVariable: string;
135
+ }[] | undefined;
136
+ } | {
137
+ id: string;
138
+ ref: {
139
+ item: string;
140
+ revision: string;
141
+ hash: string;
142
+ };
143
+ })[];
144
+ derivations: ({
145
+ id: string;
146
+ predicateIri: string;
147
+ appliesTo: string;
148
+ datatype: string;
149
+ pattern: string;
150
+ status: "query" | "staged" | "materialized";
151
+ promotedRuleIri?: string | undefined;
152
+ } | {
153
+ id: string;
154
+ ref: {
155
+ item: string;
156
+ revision: string;
157
+ hash: string;
158
+ };
159
+ })[];
160
+ actions: ({
161
+ kind: "kg-update";
162
+ insert: string;
163
+ allowedPredicates: string[];
164
+ allowedClasses: string[];
165
+ id: string;
166
+ label: string;
167
+ params: {
168
+ id: string;
169
+ type: "string" | "number" | "date" | "iri" | "enum";
170
+ required: boolean;
171
+ classIri?: string | undefined;
172
+ values?: import("zod").JSONType[] | undefined;
173
+ }[];
174
+ confirm: true;
175
+ delete?: string | undefined;
176
+ where?: string | undefined;
177
+ criteria?: {
178
+ ask: string;
179
+ } | undefined;
180
+ } | {
181
+ kind: "action-shape";
182
+ actionShapeIri: string;
183
+ operation: "update" | "addition";
184
+ mapping: Record<string, string>;
185
+ id: string;
186
+ label: string;
187
+ params: {
188
+ id: string;
189
+ type: "string" | "number" | "date" | "iri" | "enum";
190
+ required: boolean;
191
+ classIri?: string | undefined;
192
+ values?: import("zod").JSONType[] | undefined;
193
+ }[];
194
+ confirm: true;
195
+ criteria?: {
196
+ ask: string;
197
+ } | undefined;
198
+ } | {
199
+ id: string;
200
+ ref: {
201
+ item: string;
202
+ revision: string;
203
+ hash: string;
204
+ };
205
+ })[];
206
+ dependencies: {
207
+ kind: "view" | "action" | "query" | "component" | "derivation" | "horn";
208
+ ref: {
209
+ item: string;
210
+ revision: string;
211
+ hash: string;
212
+ };
213
+ }[];
214
+ lock: {
215
+ lockVersion: 1;
216
+ entries: {
217
+ kind: "view" | "action" | "query" | "component" | "derivation" | "horn";
218
+ ref: {
219
+ item: string;
220
+ revision: string;
221
+ hash: string;
222
+ };
223
+ requiredBy: string[];
224
+ }[];
225
+ };
226
+ runtime: {
227
+ kit: string;
228
+ };
229
+ summary?: string | undefined;
230
+ audience?: string | undefined;
231
+ about?: string[] | undefined;
232
+ baseRevision?: number | undefined;
233
+ style?: {
234
+ theme?: "light" | "dark" | undefined;
235
+ accent?: "blue" | "orange" | "green" | "amber" | "pink" | "forest" | "indigo" | "red" | undefined;
236
+ density?: "compact" | "comfortable" | undefined;
237
+ radius?: "sharp" | "soft" | "round" | undefined;
238
+ } | undefined;
239
+ } | undefined;
240
+ pending: boolean;
241
+ };
@@ -0,0 +1,83 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { useAgent } from "agents/react";
3
+ import { useAgentChat } from "agents/chat/react";
4
+ import { isPresent } from "@manta-eu/lib/present";
5
+ import { studioReplyOf, studioStepsOf } from "../studio-progress";
6
+ // One array, so a turn with no steps does not re-render the page every tick.
7
+ const emptySteps = [];
8
+ /** The reply and the draft arrive separately; the stream finishes first. */
9
+ const draftGraceMs = 3_000;
10
+ export function useStudioConversation(args) {
11
+ // No leading slash: the socket client builds `${host}/${basePath}` and adds the separator itself.
12
+ const at = `api/d/${encodeURIComponent(args.datastoreId)}/studio/${encodeURIComponent(args.conversation)}`;
13
+ const agent = useAgent({ agent: "studio", name: args.conversation, basePath: at });
14
+ const look = useRef(args.look);
15
+ look.current = args.look;
16
+ const chat = useAgentChat({
17
+ agent,
18
+ credentials: "include",
19
+ getInitialMessages: null,
20
+ resume: true,
21
+ tools: {
22
+ look_at_it: {
23
+ execute: (input) => {
24
+ const why = typeof input === "object" && input !== null && "why" in input ? String(input.why) : "";
25
+ return look.current(why);
26
+ },
27
+ },
28
+ },
29
+ });
30
+ const [tick, setTick] = useState(0);
31
+ const waiting = useRef(undefined);
32
+ const before = useRef(chat.status);
33
+ const drawn = useRef(undefined);
34
+ if (isPresent(agent.state?.draft))
35
+ drawn.current = agent.state.draft;
36
+ useEffect(() => {
37
+ const held = waiting.current;
38
+ if (!isPresent(held)) {
39
+ before.current = chat.status;
40
+ return;
41
+ }
42
+ const settled = chat.status === "ready" || chat.status === "error";
43
+ if (!settled) {
44
+ before.current = chat.status;
45
+ return;
46
+ }
47
+ if (chat.status === "error") {
48
+ waiting.current = undefined;
49
+ held.reject(new Error("The Studio could not answer."));
50
+ return;
51
+ }
52
+ held.settledAt ??= Date.now();
53
+ const changed = drawn.current !== held.before;
54
+ if (!changed && Date.now() - held.settledAt < draftGraceMs) {
55
+ const again = setTimeout(() => setTick((count) => count + 1), 120);
56
+ before.current = chat.status;
57
+ return () => clearTimeout(again);
58
+ }
59
+ waiting.current = undefined;
60
+ const last = [...chat.messages].reverse().find((message) => message.role === "assistant");
61
+ held.resolve({ reply: studioReplyOf(last?.parts ?? []), horn: drawn.current });
62
+ before.current = chat.status;
63
+ return;
64
+ }, [chat.status, chat.messages, tick, agent.state]);
65
+ /** Tells the object which Horn it is about. */
66
+ const open = useCallback(async (horn) => {
67
+ const response = await fetch(`/${at}/open`, {
68
+ method: "POST",
69
+ credentials: "include",
70
+ headers: { "content-type": "application/json" },
71
+ body: JSON.stringify({ horn }),
72
+ });
73
+ if (!response.ok)
74
+ throw new Error("This Horn could not be opened in the Studio.");
75
+ }, [at]);
76
+ const ask = useCallback((said) => new Promise((resolve, reject) => {
77
+ waiting.current = { resolve, reject, before: drawn.current };
78
+ void chat.sendMessage({ text: said });
79
+ }), [chat]);
80
+ const running = chat.status === "ready" ? undefined : [...chat.messages].reverse()[0];
81
+ const steps = running?.role === "assistant" ? studioStepsOf(running.parts ?? []) : emptySteps;
82
+ return { ask, open, steps, draft: agent.state?.draft ?? undefined, pending: chat.status !== "ready" };
83
+ }
@@ -3,5 +3,12 @@ export type StudioStep = {
3
3
  title: string;
4
4
  detail?: string;
5
5
  };
6
- /** One line for one streamed event, or nothing for an event not worth a line of its own. */
7
- export declare function studioProgressLine(line: Record<string, unknown>): StudioStep | undefined;
6
+ export type MessagePart = {
7
+ type: string;
8
+ toolName?: unknown;
9
+ text?: unknown;
10
+ };
11
+ /** What the turn has done so far. The same tool twice running is one line with a count. */
12
+ export declare function studioStepsOf(parts: readonly MessagePart[]): StudioStep[];
13
+ /** The answer: what was said after the last tool call. Anything before it is narration. */
14
+ export declare function studioReplyOf(parts: readonly MessagePart[]): string;
@@ -1,38 +1,63 @@
1
- import { match } from "ts-pattern";
2
- import { isRecord } from "@manta-eu/horns";
3
- // What a Horn turn is doing, in words, from the events the route streams. A tool name is the model's
4
- // vocabulary; the person watching wants to know what is being looked at.
5
1
  const toolLines = {
6
2
  list_classes: "Looking at what this datastore holds",
7
3
  describe_class: "Reading how a kind of thing is described",
8
4
  components_for_class: "Looking for components that already fit",
5
+ find_components: "Looking through the design system",
6
+ read_component_api: "Reading how a component works",
7
+ list_computed: "Looking at what Manta works out",
8
+ list_rules: "Reading the rules",
9
9
  run_sparql: "Querying the datastore",
10
10
  investigate_data: "Investigating the data",
11
+ ask_manta_for_rule: "Asking Manta for a rule",
11
12
  propose_horn_patch: "Changing the Horn",
13
+ propose_plan: "Putting a plan up",
12
14
  propose_library_item: "Offering a part for the library",
13
15
  report_harness_issue: "Noting a problem with the tools",
16
+ look_at_it: "Looking at what it drew",
14
17
  };
15
- // A turn can start a loop of its own. Its steps are the same steps, said by somebody else.
16
- const agentNames = { investigate: "Investigating" };
17
- /** One line for one streamed event, or nothing for an event not worth a line of its own. */
18
- export function studioProgressLine(line) {
19
- const event = line["event"];
20
- if (!isRecord(event))
21
- return undefined;
22
- const name = typeof event["name"] === "string" ? event["name"] : "";
23
- const detail = typeof event["detail"] === "string" ? event["detail"] : undefined;
24
- const agent = typeof event["agent"] === "string" ? (agentNames[event["agent"]] ?? event["agent"]) : undefined;
25
- const step = (title) => title === undefined
26
- ? undefined
27
- : { title: agent === undefined ? title : `${agent} · ${title}`, ...(detail === undefined ? {} : { detail }) };
28
- return match(event["type"])
29
- .with("reading", () => step("Reading the Horn and the data behind it"))
30
- .with("round", () => (event["step"] === 1 && agent === undefined ? step("Thinking") : undefined))
31
- .with("tool-call", () => event["repeated"] === true || name === "propose_horn_patch"
32
- ? undefined
33
- : step(toolLines[name] ?? `Using ${name}`))
34
- .with("proposal-accepted", () => (name === "propose_horn_patch" ? step("Changed the Horn") : undefined))
35
- .with("proposal-rejected", () => step("That change did not hold up against the data, trying again"))
36
- .with("rehydrated", () => step("Starting over from the Horn as it stands"))
37
- .otherwise(() => undefined);
18
+ // A dynamic tool part carries its name separately.
19
+ function toolNameOf(part) {
20
+ if (typeof part.toolName === "string")
21
+ return part.toolName;
22
+ return part.type.startsWith("tool-") ? part.type.slice("tool-".length) : undefined;
23
+ }
24
+ const narrationLimit = 240;
25
+ function wordsOf(part) {
26
+ return typeof part.text === "string" ? part.text.trim() : "";
27
+ }
28
+ /** What the turn has done so far. The same tool twice running is one line with a count. */
29
+ export function studioStepsOf(parts) {
30
+ const steps = [];
31
+ let repeats = 0;
32
+ let narration = "";
33
+ for (const part of parts) {
34
+ const name = toolNameOf(part);
35
+ if (name === undefined) {
36
+ const said = wordsOf(part);
37
+ if (said !== "")
38
+ narration = said;
39
+ continue;
40
+ }
41
+ const title = toolLines[name] ?? `Using ${name}`;
42
+ const detail = narration === "" ? undefined : narration.slice(0, narrationLimit);
43
+ narration = "";
44
+ const last = steps[steps.length - 1];
45
+ if (last?.title === title && detail === undefined) {
46
+ repeats += 1;
47
+ last.detail = `${repeats + 1} times`;
48
+ continue;
49
+ }
50
+ repeats = 0;
51
+ steps.push({ title, ...(detail === undefined ? {} : { detail }) });
52
+ }
53
+ return steps;
54
+ }
55
+ /** The answer: what was said after the last tool call. Anything before it is narration. */
56
+ export function studioReplyOf(parts) {
57
+ const lastTool = parts.map((part) => toolNameOf(part) !== undefined).lastIndexOf(true);
58
+ return parts
59
+ .slice(lastTool + 1)
60
+ .map(wordsOf)
61
+ .filter((said) => said !== "")
62
+ .join("\n\n");
38
63
  }