@manta-eu/horns-server 0.2.0 → 0.2.2

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.
@@ -484,6 +484,83 @@ export function hornRoutes(deps = {}) {
484
484
  * Two segments because one would not be safe: `about` is a legal slug, so `/horns/about` is a Horn
485
485
  * somebody may have saved, and `/horns/:slug` would answer for it either way.
486
486
  */
487
+ // Three fit beside a thing without pushing it off the screen; ten is where the reading cost stops being
488
+ // one query and starts being a page of them.
489
+ const defaultAboutLimit = 3;
490
+ const maxAboutLimit = 10;
491
+ /**
492
+ * Every Horn about a thing, for a host that has room for more than one.
493
+ *
494
+ * The singular route answers with one and counts the rest, which is what a panel beside a plan wants.
495
+ * A host showing a thing's whole story wants them all: `?limit=` caps how many come back whole, and
496
+ * `total` says how many there are. Nothing kept falls back the same way the singular route does, so an
497
+ * IRI nobody has made a Horn for still draws.
498
+ */
499
+ routes.get("/api/d/:datastoreId/horns/by/about/list", async (c) => {
500
+ const datastoreId = c.req.param("datastoreId");
501
+ const authorized = await authorize(c.req.raw, c.env, datastoreId);
502
+ if (!authorized.ok)
503
+ return authorized.response;
504
+ const realm = c.env.VITE_KEYCLOAK_REALM;
505
+ if (!isPresent(realm) || !isRealm(realm))
506
+ return refuse(500, "This deployment has no realm configured.").response;
507
+ const about = c.req.query("iri");
508
+ if (!isPresent(about))
509
+ return refuse(400, "Ask with ?iri=<the thing's IRI>.").response;
510
+ if (!isAbsoluteIri(about))
511
+ return refuse(400, `"${about}" is not an IRI.`).response;
512
+ const asked = Number(c.req.query("limit") ?? defaultAboutLimit);
513
+ if (!Number.isInteger(asked) || asked < 1 || asked > maxAboutLimit) {
514
+ return refuse(400, `limit is a whole number from 1 to ${maxAboutLimit}.`).response;
515
+ }
516
+ const select = agentsSparqlSelect({
517
+ agentsUrl: c.env.MANTA_AGENTS_URL,
518
+ upstream: c.env.UPSTREAM,
519
+ session: authorized.session,
520
+ datastoreId,
521
+ });
522
+ try {
523
+ const found = await select(hornsAboutQuery(realm, about), undefined);
524
+ // One row per revision, and a Horn can bring two of those, so the Horn is what is counted and the
525
+ // first row for each is the one read.
526
+ const firstRows = new Map();
527
+ for (const row of found.results.bindings) {
528
+ const hornIri = row["horn"]?.value;
529
+ if (isPresent(hornIri) && !firstRows.has(hornIri))
530
+ firstRows.set(hornIri, row);
531
+ }
532
+ if (firstRows.size === 0) {
533
+ const made = (await hornForRule({ select, realm, datastoreId, about })) ??
534
+ (await hornForPlan({ select, realm, datastoreId, about }));
535
+ return made === undefined
536
+ ? Response.json({ horns: [], total: 0 }, { headers: noStore })
537
+ : Response.json({ horns: [{ horn: made, kept: false }], total: 0 }, { headers: noStore });
538
+ }
539
+ const wanted = [...firstRows.entries()].slice(0, asked);
540
+ const read = await Promise.all(wanted.map(async ([hornIri, row]) => {
541
+ const revision = row["revision"]?.value;
542
+ if (!isPresent(revision))
543
+ return undefined;
544
+ const one = await hornAtRevision({ select, realm, hornIri, revision, number: Number(row["number"]?.value) });
545
+ if (one === undefined)
546
+ return undefined;
547
+ const slug = row["slug"]?.value;
548
+ return {
549
+ horn: one.horn,
550
+ kept: true,
551
+ revision,
552
+ contentHash: await contentHash(one.stored),
553
+ ...(isPresent(slug) ? { slug } : {}),
554
+ };
555
+ }));
556
+ // A revision that no longer reads as a Horn is left out rather than failing the lot: the others are
557
+ // still worth drawing, and `total` already says more exist.
558
+ return Response.json({ horns: read.filter(isPresent), total: firstRows.size }, { headers: noStore });
559
+ }
560
+ catch (caught) {
561
+ return refuse(502, caught instanceof Error ? caught.message : String(caught)).response;
562
+ }
563
+ });
487
564
  routes.get("/api/d/:datastoreId/horns/by/about", async (c) => {
488
565
  const datastoreId = c.req.param("datastoreId");
489
566
  const authorized = await authorize(c.req.raw, c.env, datastoreId);
@@ -23,6 +23,8 @@ export declare class HornStudioAgent extends Think<StudioAgentEnv, StudioAgentSt
23
23
  onConnect(connection: Connection, ctx: ConnectionContext): void | Promise<void>;
24
24
  private get opened();
25
25
  private get realm();
26
+ /** How a turn reaches agents, as the viewer and through the host's own upstream where it has one. */
27
+ private get caller();
26
28
  private select;
27
29
  getModel(): LanguageModel;
28
30
  /** Where the draft has got to: nothing drawn is designing, drawn but unbound is hydrating. */
@@ -42,13 +42,17 @@ export class HornStudioAgent extends Think {
42
42
  get realm() {
43
43
  return this.opened.realm;
44
44
  }
45
- select() {
46
- return agentsSparqlSelect({
45
+ /** How a turn reaches agents, as the viewer and through the host's own upstream where it has one. */
46
+ get caller() {
47
+ return {
47
48
  agentsUrl: this.env.MANTA_AGENTS_URL,
48
49
  upstream: this.env.UPSTREAM,
49
50
  session: this.opened.session,
50
51
  datastoreId: this.opened.datastoreId,
51
- });
52
+ };
53
+ }
54
+ select() {
55
+ return agentsSparqlSelect(this.caller);
52
56
  }
53
57
  getModel() {
54
58
  const chosen = studioModel(this.env);
@@ -116,11 +120,7 @@ export class HornStudioAgent extends Think {
116
120
  lookup: () => undefined,
117
121
  ...(deps.typecheck ? { typecheck: deps.typecheck } : {}),
118
122
  // As the viewer: staging a rule is attributed to a person, and agents refuses it otherwise.
119
- stageRule: (rule) => stageRule({
120
- agentsUrl: this.env.MANTA_AGENTS_URL,
121
- session: this.opened.session,
122
- datastoreId: this.opened.datastoreId,
123
- }, rule),
123
+ stageRule: (rule) => stageRule(this.caller, rule),
124
124
  }),
125
125
  };
126
126
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manta-eu/horns-server",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "The Manta Horns worker as a library: Hono routes a Cloudflare host mounts on its own app.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -59,8 +59,8 @@
59
59
  "@ai-sdk/google-vertex": "^5.0.101",
60
60
  "@ai-sdk/openai-compatible": "^3.0.62",
61
61
  "@cloudflare/think": "0.19.0",
62
- "@manta-eu/client": "0.1.5",
63
- "@manta-eu/lib": "0.1.5",
62
+ "@manta-eu/client": "0.1.7",
63
+ "@manta-eu/lib": "0.1.7",
64
64
  "@sentry/cloudflare": "^10.70.0",
65
65
  "@sentry/core": "^10.70.0",
66
66
  "agents": "^0.24.0",
@@ -70,8 +70,8 @@
70
70
  "sparqljs": "^3.7.4",
71
71
  "typescript": "^5.9.0",
72
72
  "zod": "^4.5.4",
73
- "@manta-eu/horns": "0.2.0",
74
- "@manta-eu/horns-kit": "0.2.0"
73
+ "@manta-eu/horns": "0.2.2",
74
+ "@manta-eu/horns-kit": "0.2.2"
75
75
  },
76
76
  "devDependencies": {
77
77
  "@cloudflare/vite-plugin": "1.47.0",