@hanzo/rooms 0.1.38 → 0.1.40

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/lib/host.js CHANGED
@@ -1,18 +1,20 @@
1
1
  /**
2
- * Which host this export is read on, and what that host keeps.
2
+ * Which host this export is read on, what that host keeps, and the app's
3
+ * addresses — the one module that writes and reads them.
3
4
  *
4
5
  * hanzo.ai is the site for a stranger and the app for a member: the web app is
5
- * `/` once signed in (app/page.tsx), beside /chat. hanzo.team is the
6
- * workspace, its own app (hanzo-apps/team) with Home at its root. This export
7
- * still carries the rooms, so a room asked for on the apex is sent to the same
8
- * room on hanzo.team, and `output: 'export'` has no server to do that, so it is
9
- * decided in the browser off the hostname, here.
6
+ * /chat and /dev once signed in, and `/` takes a member to the mode they were
7
+ * last in. hanzo.team is the workspace, its own app (hanzo-apps/team) with Home
8
+ * at its root. This export still carries the rooms, so a room asked for on the
9
+ * apex is sent to the same room on hanzo.team, and `output: 'export'` has no
10
+ * server to do that, so it is decided in the browser off the hostname, here.
10
11
  *
11
12
  * Two predicates, one per product: `apex()` for the app, `team()` for the
12
13
  * workspace. Every host that is neither (localhost, a preview) draws the whole
13
14
  * workspace and admits the way the apex does.
14
15
  */
15
16
  import APP from './rooms.json' with { type: 'json' };
17
+ import { href, parse, path, route, SESSION, SLUG } from '@hanzo/build/route';
16
18
  import { site } from '../where.js';
17
19
  /** The workspace's own address. */
18
20
  export const WORKSPACE = 'https://hanzo.team';
@@ -32,59 +34,107 @@ export const ENTRY = `${WORKSPACE}/?signin`;
32
34
  */
33
35
  export const START = `${WORKSPACE}/start`;
34
36
  /**
35
- * The web app, for a signed-in reader, in two modes. Chat is the default, at
36
- * `/`. Dev is @hanzo/build's `<Builder>` at `/dev`, and what it shows is the
37
- * PATH under it, in the builder's own grammar (`route` in @hanzo/build): `/dev`
38
- * New, `/dev/sess_<id>` a run, `/dev/<org>/<repo>` a project,
39
- * `/dev/-/<screen>`, `/dev/-/settings[/<section>]`, `/dev/-/customize[/<tab>]`,
40
- * `/dev/<slug>` a deployed site. The export holds one page for all of them,
41
- * `/dev`, and the edge serves it for every path under it (hanzo.ai's
42
- * lib/edge.ts `APP`).
37
+ * THE APP'S ADDRESSES ARE PATHS, in two modes at two roots. Chat is /chat, a
38
+ * new conversation, and /chat/<id> one of the reader's. Dev is /dev, Dev's New,
39
+ * and every place of @hanzo/build's builder is a path under it, written and
40
+ * read by `href`/`parse` (@hanzo/build/route): /dev/run/<id>, /dev/projects,
41
+ * /dev/projects/<org>/<repo> a repository, /dev/projects/<slug> a deployed
42
+ * site, /dev/issues, /dev/artifacts, /dev/templates, /dev/automations[/<id>],
43
+ * /dev/machines, /dev/environments, /dev/customize[/<tab>],
44
+ * /dev/settings[/<section>], /dev/plans. The export holds one page per root,
45
+ * /chat and /dev, and the edge serves it for every path under it (hanzo.ai's
46
+ * lib/edge.ts `APP`), so every address reloads.
43
47
  */
48
+ export const CHAT = '/chat';
44
49
  export const DEV = '/dev';
45
- /** Dev at an app address; '' is Dev's New, `/dev` itself. */
46
- export const app = (path) => {
47
- const p = path.replace(/^\/+|\/+$/g, '');
48
- return p ? `${DEV}/${p}` : DEV;
49
- };
50
- /** The app address a pathname names under `/dev`, or null for a pathname that is not Dev's. */
51
- export const under = (pathname) => {
52
- if (!pathname)
50
+ /** Chat on a conversation, or /chat itself for a new one. */
51
+ export const talk = (id) => (id ? `${CHAT}/${encodeURIComponent(id)}` : CHAT);
52
+ /** Dev at a builder address (`route` in @hanzo/build); '' is New, /dev. What is not an address is New. */
53
+ export const app = (at) => DEV + href(route(at));
54
+ /** /chat/shared is a page of its own, where a chat shared by link is read (lib/share.ts). */
55
+ const SHARED = 'shared';
56
+ /** The place a path names, or null when it names none of the app's. */
57
+ export function place(pathname) {
58
+ const p = pathname.replace(/\/+$/, '') || '/';
59
+ if (p === CHAT)
60
+ return { mode: 'chat', thread: null };
61
+ if (p === DEV || p.startsWith(`${DEV}/`))
62
+ return { mode: 'dev', at: path(parse(p.slice(DEV.length))) };
63
+ if (!p.startsWith(`${CHAT}/`))
53
64
  return null;
54
- if (pathname === DEV || pathname === `${DEV}/`)
55
- return '';
56
- if (!pathname.startsWith(`${DEV}/`))
65
+ const id = p.slice(CHAT.length + 1);
66
+ if (id.includes('/') || id === SHARED)
57
67
  return null;
58
68
  try {
59
- return decodeURIComponent(pathname.slice(DEV.length + 1));
69
+ return { mode: 'chat', thread: decodeURIComponent(id) };
60
70
  }
61
71
  catch {
62
- return '';
72
+ return null;
63
73
  }
64
- };
65
- /**
66
- * Chat, the app's default mode: `/` is a new conversation and `?chat=<id>` one
67
- * of the reader's. So the address says which mode the app is in — a path under
68
- * `/dev` for Dev, anything else Chat — and a reload keeps it.
69
- */
70
- export const TALK = 'chat';
71
- /** `/` in Chat, on a conversation, or `/` itself for a new one. */
72
- export const talk = (id) => (id ? `/?${TALK}=${encodeURIComponent(id)}` : '/');
73
- /** A run, as the platform mints its id. */
74
- const SESSION = /^sess_[0-9a-f]{32}$/;
75
- /** A deployed site, as the Sites plane slugs it. */
76
- const SLUG = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/;
74
+ }
75
+ /** An address as analytics counts it: a record folded to its kind, so every conversation is one page. */
76
+ export function page(pathname) {
77
+ const at = place(pathname);
78
+ if (!at)
79
+ return pathname;
80
+ if (at.mode === 'chat')
81
+ return at.thread ? `${CHAT}/:id` : CHAT;
82
+ const r = route(at.at);
83
+ if (r.kind === 'run')
84
+ return `${DEV}/run/:id`;
85
+ if (r.kind === 'repo')
86
+ return `${DEV}/projects/:org/:repo`;
87
+ if (r.kind === 'project')
88
+ return `${DEV}/projects/:slug`;
89
+ if (r.kind === 'automation' && r.id !== 'new')
90
+ return `${DEV}/automations/:id`;
91
+ return DEV + href(r);
92
+ }
77
93
  /** A repository on the forge, `<org>/<name>`. */
78
94
  const REPO = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}\/[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
79
95
  /**
80
- * Dev opened on one run, project or site, from anywhere: every "Try Hanzo Dev"
81
- * link, a room's Dev, the console's "open in the builder". It signs a stranger
82
- * in on the way (app/(app)/dev/_door.tsx), and is reached through `site()`:
83
- * here on hanzo.ai, across to hanzo.ai from hanzo.team. Only a value that is
84
- * exactly a session id, a repository or a site's slug is carried; anything else
85
- * opens /dev itself, so no caller can make this name another address.
96
+ * Dev opened on one run, repository or site, from anywhere: every "Try Hanzo
97
+ * Dev" link, a room's Dev, the console's "open in the builder". It signs a
98
+ * stranger in on the way (app/(app)/dev/_door.tsx), and is reached through
99
+ * `site()`: here on hanzo.ai, across to hanzo.ai from hanzo.team. Only a value
100
+ * that is exactly a session id, a repository or a site's slug is carried;
101
+ * anything else opens /dev itself, so no caller can make this name another
102
+ * address.
86
103
  */
87
- export const dev = (ref) => site(ref && (SESSION.test(ref) || REPO.test(ref) || SLUG.test(ref)) ? app(ref) : DEV);
104
+ export const dev = (ref) => site(app(ref && (SESSION.test(ref) || REPO.test(ref) || SLUG.test(ref)) ? ref : ''));
105
+ /**
106
+ * Move the app to an address of this page: a history entry, or in place of this
107
+ * one. No document load and no route change — the Next router folds a native
108
+ * history entry into `usePathname`, so the app follows the path it reads.
109
+ */
110
+ export function go(address, replace = false) {
111
+ if (`${window.location.pathname}${window.location.search}` === address)
112
+ return;
113
+ if (replace)
114
+ window.history.replaceState(null, '', address);
115
+ else
116
+ window.history.pushState(null, '', address);
117
+ }
118
+ /** Where this browser keeps the mode its reader was last in. */
119
+ const MODE = 'hanzo.app.mode';
120
+ /** Keep the mode the reader is in, for `resume`. */
121
+ export function remember(mode) {
122
+ try {
123
+ window.localStorage.setItem(MODE, mode);
124
+ }
125
+ catch {
126
+ /* no store: `/` opens Chat */
127
+ }
128
+ }
129
+ /** Where `/` takes a member: the mode they were last in, Chat when none is kept. */
130
+ export function resume() {
131
+ try {
132
+ return window.localStorage.getItem(MODE) === 'dev' ? DEV : CHAT;
133
+ }
134
+ catch {
135
+ return CHAT;
136
+ }
137
+ }
88
138
  /** The routes under app/(app) the apex keeps: the app — /chat, a chat shared by link, and /dev. */
89
139
  const KEPT = new Set(['/chat', '/chat/shared', '/dev']);
90
140
  /** Settings, and every page under it: the person's and the organization's, served wherever the app is. */
@@ -135,7 +185,7 @@ export const GET = 'https://hanzo.app';
135
185
  * is signed out is asked to sign in there, on hanzo.ai's own page, and lands
136
186
  * back in the chat.
137
187
  */
138
- export const WEB = `${SIGNIN}/chat`;
188
+ export const WEB = `${SIGNIN}${CHAT}`;
139
189
  /**
140
190
  * Whether a host serves this export and signs nobody in: hanzo.app, whose root
141
191
  * is the page above; every `<slug>.hanzo.app` alias the Sites plane serves it
@@ -1,13 +1,5 @@
1
- /**
2
- * The plan's usage, as @hanzo/ui holds it: `GET /v1/ai/limits` read as whoever
3
- * holds the bearer in this browser's organization, kept current by every AI
4
- * call `observed` (lib/served.ts) hands to `observe`. Shares only.
5
- *
6
- * Same base URL and headers as every billing read (lib/hanzo/tier.ts): the
7
- * bearer is the subject and `X-Org-Id` the organization this browser works in.
8
- */
9
1
  import { type UseLimits } from '@hanzo/ui/product/useLimits';
10
- import { paidPlan } from '@hanzo/ui/product/limits';
2
+ import { paidPlan, type LimitAction, type LimitNotice } from '@hanzo/ui/product/limits';
11
3
  /** Reads the limits as whoever holds the bearer, in this browser's organization. */
12
4
  export declare function readLimits(signal: AbortSignal): Promise<unknown>;
13
5
  /**
@@ -28,6 +20,18 @@ export declare const useLimits: (enabled?: boolean, org?: string | null, name?:
28
20
  */
29
21
  export declare const spendShown: ({ limits, answered }: Pick<UseLimits, 'limits' | 'answered'>) => boolean;
30
22
  export { paidPlan };
23
+ /**
24
+ * What the banner over the composer says once the reader is turned away, and
25
+ * the ways on. A spent allowance is said by the plan's name and when it comes
26
+ * back — `Your Max 20x plan’s included premium model usage is used until 5:00
27
+ * PM.` — and a spent request window the same way; any other refusal in the
28
+ * server's own words. The ways are @hanzo/build's `ways`: continue with credits
29
+ * where the org holds some, else add credits; then the upgrade.
30
+ */
31
+ export declare function paused(n: LimitNotice, plan: string, name?: (id: string) => string, now?: number): {
32
+ message: string;
33
+ actions: LimitAction[];
34
+ };
31
35
  /** Whether a catalog family is one of Hanzo's chat families, Enso or Zen, whose turns carry the live web. */
32
36
  export declare const house: (family: string | undefined) => boolean;
33
37
  /**
@@ -7,6 +7,7 @@
7
7
  * Same base URL and headers as every billing read (lib/hanzo/tier.ts): the
8
8
  * bearer is the subject and `X-Org-Id` the organization this browser works in.
9
9
  */
10
+ import { label, said, spent, ways, when } from '@hanzo/build/plan';
10
11
  import { useLimits as useHeld } from '@hanzo/ui/product/useLimits';
11
12
  import { paidPlan } from '@hanzo/ui/product/limits';
12
13
  import { api } from './api.js';
@@ -47,6 +48,25 @@ export const useLimits = (enabled = true, org = null, name) => useHeld(enabled ?
47
48
  */
48
49
  export const spendShown = ({ limits, answered }) => answered && !paidPlan(limits);
49
50
  export { paidPlan };
51
+ /**
52
+ * What the banner over the composer says once the reader is turned away, and
53
+ * the ways on. A spent allowance is said by the plan's name and when it comes
54
+ * back — `Your Max 20x plan’s included premium model usage is used until 5:00
55
+ * PM.` — and a spent request window the same way; any other refusal in the
56
+ * server's own words. The ways are @hanzo/build's `ways`: continue with credits
57
+ * where the org holds some, else add credits; then the upgrade.
58
+ */
59
+ export function paused(n, plan, name = (id) => id, now = Date.now()) {
60
+ const l = label(plan);
61
+ const until = when(n.resets_at ?? '', now);
62
+ const on = n.fallback ? ` You’re chatting on ${name(n.fallback)}.` : '';
63
+ const message = n.reason === 'plan_allowance_used' || (!n.reason && n.classes.length > 0)
64
+ ? spent(l, n.classes, n.resets_at ?? '', now) + on
65
+ : n.reason === 'usage_cap_exceeded'
66
+ ? `Your ${l ? `${said(l)} plan’s` : 'plan’s'} requests are used${until ? ` until ${until}` : ''}.${on}`
67
+ : n.message;
68
+ return { message, actions: ways(n.actions) };
69
+ }
50
70
  /** Whether a catalog family is one of Hanzo's chat families, Enso or Zen, whose turns carry the live web. */
51
71
  export const house = (family) => family === 'enso' || family === 'zen';
52
72
  /**
@@ -26,8 +26,8 @@
26
26
  * A SuperAdmin edits these rows at admin.hanzo.ai (/v1/commerce/plans/entries),
27
27
  * and every reader here follows: the build bakes the authority's answer into the
28
28
  * pricing snapshot for the first paint, and the page replaces it with the live
29
- * read. No price in this repo is typed; @hanzo/plans is read only for what the
30
- * wire does not carry — a plan's name for a slug, and the Free row's models.
29
+ * read. No price in this repo is typed, and no plan's name: a slug is named by
30
+ * @hanzo/build's `label` (`max-20x` reads Max, with 20x beside it).
31
31
  */
32
32
  /** A row exactly as GET /v1/billing/plans returns it. */
33
33
  export interface BillingPlan {
@@ -150,17 +150,6 @@ export declare function charge(plan: SubscriptionPlan, interval: Interval): numb
150
150
  export declare function quote(plan: SubscriptionPlan, interval: Interval): string | null;
151
151
  /** What a year saves against twelve months, as a whole percent: 1 − year ÷ (12 × month). Null where it saves nothing. */
152
152
  export declare function saving(plan: SubscriptionPlan): number | null;
153
- /**
154
- * The catalog's own name for a plan slug — "max-20x" reads as "Max 20x" —
155
- * or null where the slug is empty or the catalog does not carry it.
156
- *
157
- * Read off the published package: a plan's name does not move between a build and the next release the way
158
- * its price can, so the bundled copy is not a stale answer here the way it
159
- * would be for money. A reader of `GET /v1/billing/tier`'s `plan` field
160
- * needs a name and nothing else, so no live fetch is spent proving what the
161
- * package already states.
162
- */
163
- export declare function planName(slug: string | null | undefined): string | null;
164
153
  /**
165
154
  * A usage rate as money. A sub-dollar rate needs at least two decimals to read as
166
155
  * a price at all — 0.1 is $0.10, not $0.1 — while keeping the precision the
package/dist/lib/plans.js CHANGED
@@ -26,11 +26,10 @@
26
26
  * A SuperAdmin edits these rows at admin.hanzo.ai (/v1/commerce/plans/entries),
27
27
  * and every reader here follows: the build bakes the authority's answer into the
28
28
  * pricing snapshot for the first paint, and the page replaces it with the live
29
- * read. No price in this repo is typed; @hanzo/plans is read only for what the
30
- * wire does not carry — a plan's name for a slug, and the Free row's models.
29
+ * read. No price in this repo is typed, and no plan's name: a slug is named by
30
+ * @hanzo/build's `label` (`max-20x` reads Max, with 20x beside it).
31
31
  */
32
32
  import { useEffect, useMemo, useState } from "react";
33
- import { subscriptionPlans } from "@hanzo/plans";
34
33
  import { where } from "../where.js";
35
34
  import { api } from "./api.js";
36
35
  import { BILLING_URL, checkoutUrl, dollars, money, payUrl } from "./pay.js";
@@ -222,22 +221,6 @@ export function saving(plan) {
222
221
  return null;
223
222
  return Math.round((1 - year / (12 * month)) * 100);
224
223
  }
225
- /**
226
- * The catalog's own name for a plan slug — "max-20x" reads as "Max 20x" —
227
- * or null where the slug is empty or the catalog does not carry it.
228
- *
229
- * Read off the published package: a plan's name does not move between a build and the next release the way
230
- * its price can, so the bundled copy is not a stale answer here the way it
231
- * would be for money. A reader of `GET /v1/billing/tier`'s `plan` field
232
- * needs a name and nothing else, so no live fetch is spent proving what the
233
- * package already states.
234
- */
235
- export function planName(slug) {
236
- if (!slug)
237
- return null;
238
- const row = subscriptionPlans.find((p) => p.id === slug);
239
- return row?.name ?? null;
240
- }
241
224
  /**
242
225
  * A usage rate as money. A sub-dollar rate needs at least two decimals to read as
243
226
  * a price at all — 0.1 is $0.10, not $0.1 — while keeping the precision the
@@ -31,18 +31,15 @@ export declare function bearer(): string | null;
31
31
  export declare function live(): boolean;
32
32
  /** The attribute on `<html>` that says `/` is showing the app, not the landing. */
33
33
  export declare const APP = "data-app";
34
- /** Whether `search` names a place in the app. */
35
- export declare const place: (search: string) => boolean;
36
- /**
37
- * `live()`, and a place in the app, as the script `/` runs as the first child
38
- * of its landing (app/_apex.tsx `[data-landing]`), before the rest of the
39
- * landing is parsed (app/page.tsx). Either marks the document with `APP` and
40
- * takes the landing off the page at once: a signed-in reader is never shown the
41
- * pitch while the app loads, and a reader on their way to sign in to a place is
42
- * not shown it while they leave. Same keys, same test. An inline style, because
43
- * this is decided before any stylesheet or component has run.
44
- */
45
- export declare const BOOT: string;
34
+ /**
35
+ * `live()`, as the script `/` runs as the first child of its landing
36
+ * (app/_apex.tsx `[data-landing]`), before the rest of the landing is parsed
37
+ * (app/page.tsx). A live token marks the document with `APP` and takes the
38
+ * landing off the page at once, so a signed-in reader is never shown the pitch
39
+ * on the way to the app. Same keys, same test. An inline style, because this is
40
+ * decided before any stylesheet or component has run.
41
+ */
42
+ export declare const BOOT = "try{var t=localStorage.getItem('hanzo_iam_access_token'),e=localStorage.getItem('hanzo_iam_expires_at');if(t&&(!e||Date.now()<Number(e))){document.documentElement.setAttribute('data-app','');var l=document.currentScript&&document.currentScript.parentElement;if(l&&l.hasAttribute('data-landing'))l.style.display='none'}}catch(_){}";
46
43
  /**
47
44
  * Whether this tab may still ask: a person's browser, and not asked in this tab
48
45
  * yet. A browser that refuses storage cannot remember having asked, so it never
@@ -14,7 +14,6 @@
14
14
  * who was signed in, which reads as a permissions problem rather than a typo.
15
15
  * Two derivations of one name never agree; this is the one derivation.
16
16
  */
17
- import { TALK } from './host.js';
18
17
  import { api } from './api.js';
19
18
  /** The SDK's own keys, `hanzo_iam_`-prefixed. The far side of a boundary. */
20
19
  const PREFIX = 'hanzo_iam_';
@@ -59,25 +58,14 @@ export function live() {
59
58
  /** The attribute on `<html>` that says `/` is showing the app, not the landing. */
60
59
  export const APP = 'data-app';
61
60
  /**
62
- * An address on `/` that names a place in the app — `?chat` in Chat
63
- * (lib/host.ts `TALK`; Dev's places are paths under `/dev`, whose own page
64
- * signs a stranger in). The app is signed-in only, so a reader nobody has
65
- * signed in is sent to hanzo.id with it rather than shown the landing, and it
66
- * comes back with them.
61
+ * `live()`, as the script `/` runs as the first child of its landing
62
+ * (app/_apex.tsx `[data-landing]`), before the rest of the landing is parsed
63
+ * (app/page.tsx). A live token marks the document with `APP` and takes the
64
+ * landing off the page at once, so a signed-in reader is never shown the pitch
65
+ * on the way to the app. Same keys, same test. An inline style, because this is
66
+ * decided before any stylesheet or component has run.
67
67
  */
68
- const PLACE = new RegExp(`[?&]${TALK}(?:[=&]|$)`);
69
- /** Whether `search` names a place in the app. */
70
- export const place = (search) => PLACE.test(search);
71
- /**
72
- * `live()`, and a place in the app, as the script `/` runs as the first child
73
- * of its landing (app/_apex.tsx `[data-landing]`), before the rest of the
74
- * landing is parsed (app/page.tsx). Either marks the document with `APP` and
75
- * takes the landing off the page at once: a signed-in reader is never shown the
76
- * pitch while the app loads, and a reader on their way to sign in to a place is
77
- * not shown it while they leave. Same keys, same test. An inline style, because
78
- * this is decided before any stylesheet or component has run.
79
- */
80
- export const BOOT = `try{var t=localStorage.getItem('${ACCESS}'),e=localStorage.getItem('${EXPIRES}');if((t&&(!e||Date.now()<Number(e)))||${PLACE}.test(location.search)){document.documentElement.setAttribute('${APP}','');var l=document.currentScript&&document.currentScript.parentElement;if(l&&l.hasAttribute('data-landing'))l.style.display='none'}}catch(_){}`;
68
+ export const BOOT = `try{var t=localStorage.getItem('${ACCESS}'),e=localStorage.getItem('${EXPIRES}');if(t&&(!e||Date.now()<Number(e))){document.documentElement.setAttribute('${APP}','');var l=document.currentScript&&document.currentScript.parentElement;if(l&&l.hasAttribute('data-landing'))l.style.display='none'}}catch(_){}`;
81
69
  /**
82
70
  * A RETURNING PERSON IS RECOGNISED WITHOUT GOING ANYWHERE.
83
71
  *
package/dist/lib/web.d.ts CHANGED
@@ -1,48 +1,56 @@
1
1
  /**
2
2
  * THE LIVE WEB, IN A CHAT TURN.
3
3
  *
4
- * A turn on an Enso or Zen model is offered two tools, `websearch` and `crawl`.
5
- * They are the platform's own operations — POST /v1/websearch and POST
6
- * /v1/crawl, the two the assistant calls when it answers in Slack — run here
7
- * with the reader's own credential, so they are admitted, metered and refused
8
- * exactly as they are for every other caller.
4
+ * A turn on an Enso or Zen model carries the two web tools (`WEB`) as its mark
5
+ * that it may use the web. `researched` is the client's fetch that acts on the
6
+ * mark: it looks the turn up BEFORE the model is asked — POST /v1/websearch and
7
+ * POST /v1/crawl, the platform's own operations, run with the reader's own
8
+ * credential so they are admitted, metered and refused exactly as for every
9
+ * other caller — and then asks the model once, WITHOUT tools, with what was
10
+ * found in its system turn.
9
11
  *
10
- * `researched` is the client's fetch with the tool loop in it. A completion
11
- * that offers these tools is answered as ONE event stream: the model asks for a
12
- * lookup, the lookup runs, its result goes back, and the model is asked again —
13
- * each round an ordinary completion, so the plan's windows count it like any
14
- * other — and the reader's stream carries only what the model says. Nothing
15
- * above the fetch changes: `useChat` streams the answer it always streamed.
12
+ * Without tools because the gateway answers a request that offers tools whole,
13
+ * checking its calls against their schemas before the first byte, so its first
14
+ * word waits for its last. Asked without them, every turn streams. The model
15
+ * therefore never asks for a lookup of its own: the client's one lookup is the
16
+ * turn's web.
16
17
  *
17
- * A question about now — the weather, the news, a price, a score, "today",
18
- * "latest" — does not wait for the model to decide: it is searched before the
19
- * model is asked (`current`), and the search reads the best pages as well as
20
- * listing them, so the answer can quote a reading rather than a page's blurb.
18
+ * Which turns: every question about the world, in any language. The web is
19
+ * skipped only for a turn that plainly needs none (`needs`) — code, arithmetic,
20
+ * a passage supplied to work on, a greeting — and for a turn about the person's
21
+ * own files or picture, whose words never leave for a search engine. The test
22
+ * reads the shape of the text, never a list of topics. A turn that names a page
23
+ * reads it, and searches too when it asks something. The query is the person's
24
+ * own words, each address said as its host and path words, and the language narrows
25
+ * the search (`language`), so a question asked in Russian is searched in Russian.
21
26
  *
22
- * Only a turn that needs the web is offered it: a question about now, or one
23
- * that names a page. The gateway answers a request that offers tools whole,
24
- * after checking its calls against their schemas, so its first word waits for
25
- * its last; every other turn goes out without the tools and streams.
27
+ * WHERE WHAT WAS FOUND GOES: the end of the leading system turn, between two
28
+ * markers no page can know. Not a system turn of its own, because the gateway's
29
+ * text pipeline keeps only the last system turn it reads, and that would drop
30
+ * the room's own; not the person's turn, because the router chooses the model
31
+ * from the last user turn and would read the web pages as the question.
26
32
  *
27
- * Bounded: at most ROUNDS rounds of lookups the model asks for, CALLS lookups in
28
- * a round, and every lookup on its own clock. The request after the last round
29
- * offers no tools, so the turn ends in words; a reply of no words is asked for
30
- * once more.
31
- *
32
- * A page is read only when the conversation or one of this turn's searches
33
- * named it. Page text is written by strangers and can tell a model to fetch an
34
- * address carrying the conversation in its query; an address nobody named is
35
- * never fetched.
33
+ * A page is read only when the person points at it — never an address that
34
+ * signs in, confirms, unsubscribes or spends a token (`opened`) — or this turn's
35
+ * search found it. A reply of no words is asked for once more.
36
36
  */
37
37
  import { type ChatCompletionTool } from '@hanzo/ai';
38
- /** Rounds of lookups the model may ask for in one turn. */
39
- export declare const ROUNDS = 2;
40
- /** Lookups run in one round; the rest are answered as refused. */
41
- export declare const CALLS = 3;
42
- /** The two tools a turn is offered. */
38
+ /** Pages a turn that names them reads. */
39
+ export declare const PAGES = 3;
40
+ /** The two tools that mark a turn as one that may use the web. */
43
41
  export declare const WEB: ChatCompletionTool[];
44
- /** Whether a question asks about now. */
45
- export declare const current: (text: string) => boolean;
42
+ /**
43
+ * Whether a turn's words need the web. Every question about the world does, in
44
+ * any language; the turns that plainly do not are code, arithmetic, a passage
45
+ * supplied to work on, a greeting, and a turn of no words at all.
46
+ */
47
+ export declare function needs(text: string): boolean;
48
+ /**
49
+ * The language a question is written in, as /v1/websearch reads it ("ru",
50
+ * "en", "ja"), or '' when its words do not say. `spoken` is the reader's own
51
+ * languages, which pick among the languages a script is shared by.
52
+ */
53
+ export declare function language(text: string, spoken?: readonly string[]): string;
46
54
  /**
47
55
  * The part of a page that answers, within `max` characters: the lines that
48
56
  * carry the question's words or a reading — a figure, a unit — kept in the
@@ -50,7 +58,7 @@ export declare const current: (text: string) => boolean;
50
58
  */
51
59
  export declare function excerpt(markdown: string, words: string[], max: number): string;
52
60
  /**
53
- * A fetch that runs the web tools a completion offers (see the head of this
54
- * file). Every other request passes through untouched.
61
+ * A fetch that looks up the turns a completion marks with the web tools (see
62
+ * the head of this file). Every other request passes through untouched.
55
63
  */
56
- export declare function researched(base: typeof fetch, clock?: () => Date): typeof fetch;
64
+ export declare function researched(base: typeof fetch, clock?: () => Date, spoken?: () => readonly string[]): typeof fetch;