@haikit/server 0.11.0 → 0.13.0
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/README.md +74 -0
- package/dist/routes.d.ts +9 -6
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +166 -8
- package/dist/routes.js.map +1 -1
- package/dist/runtime.d.ts +30 -4
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +151 -9
- package/dist/runtime.js.map +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +69 -0
- package/dist/store.js.map +1 -1
- package/package.json +2 -2
- package/src/routes.ts +167 -10
- package/src/runtime.ts +169 -9
- package/src/store.ts +65 -0
package/README.md
CHANGED
|
@@ -47,6 +47,27 @@ call that asks and then throws has its question withdrawn, so another call in
|
|
|
47
47
|
the same reply can ask instead. Every render a tool starts finishes before its
|
|
48
48
|
call is decided, and a render after the tool has returned shows nothing.
|
|
49
49
|
|
|
50
|
+
**A slow tool can say how far it has got.** `ctx.progress` sends the browser
|
|
51
|
+
a frame for the tool's row, and nothing else: it isn't stored, and the model
|
|
52
|
+
never sees it.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
run: async (input, ctx) => {
|
|
56
|
+
ctx.progress({ message: "Checking fare sources", done: 0, total: sources.length });
|
|
57
|
+
for (const [i, source] of sources.entries()) {
|
|
58
|
+
rows.push(...(await source.search(input)));
|
|
59
|
+
ctx.progress({ done: i + 1 }); // a call updates only the fields it names
|
|
60
|
+
}
|
|
61
|
+
return ctx.render(flightTableServer, { ...input, flights: rows }, { mode: "elicit" });
|
|
62
|
+
};
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Every field is optional. Frames go out at most every 100 ms, so a tool can
|
|
66
|
+
report every row; the last one always goes out, before the row says the call
|
|
67
|
+
has finished. A call after the tool has returned does nothing, and a field
|
|
68
|
+
that isn't what its type says is dropped rather than failing the tool:
|
|
69
|
+
`done` must be zero or more, and `total` more than zero.
|
|
70
|
+
|
|
50
71
|
**`init` runs a tool at the start of every conversation**, before the user
|
|
51
72
|
types anything. The runtime makes the call, not the model, so it can't be
|
|
52
73
|
skipped: the call and its result go into the history as if the model had made
|
|
@@ -92,6 +113,59 @@ tools it was given, and the browser can only send `{handle, action, value}`,
|
|
|
92
113
|
so put every check with consequences, such as who may book what, inside the
|
|
93
114
|
tool or action handler, where it runs as code.
|
|
94
115
|
|
|
116
|
+
**The server can tell a conversation something later, with a notice.** A
|
|
117
|
+
webhook confirming a booking, a job finishing: `hai.notify` sends the browser
|
|
118
|
+
the notice's payload now, and the model its `model` text in the next user
|
|
119
|
+
message the conversation records. Declare it once, beside your surfaces:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
// shared/notices.ts — imported by both halves
|
|
123
|
+
export const holdConfirmed = defineNotice({
|
|
124
|
+
name: "hold_confirmed",
|
|
125
|
+
version: 1,
|
|
126
|
+
payload: z.object({ flightId: z.string(), reference: z.string() }),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// server
|
|
130
|
+
const holdConfirmedServer = holdConfirmed.implement({
|
|
131
|
+
// required, like a digest: what the model hears, or null for nothing
|
|
132
|
+
model: (p) => `The airline confirmed the hold on ${p.flightId}, reference ${p.reference}.`,
|
|
133
|
+
});
|
|
134
|
+
const hai = createHai({ model, store, tools, surfaces, system, notices: [holdConfirmedServer] });
|
|
135
|
+
|
|
136
|
+
// anywhere, whenever: a webhook, a queue, an action handler's follow-up
|
|
137
|
+
await hai.notify(conversationId, holdConfirmedServer, { flightId: "AC832", reference: "QX7P2L" }, { handle: "ui_01" });
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The payload is typed from the contract, and checked against its schema before
|
|
141
|
+
it is stored. A notice that isn't listed in `notices`, a payload that fails its
|
|
142
|
+
schema, or a `handle` with no surface stored in the conversation is refused.
|
|
143
|
+
Pass a handle from the conversation's saved history, such as an action
|
|
144
|
+
handler's `ctx.handle`. The check is against the stored surfaces, not that
|
|
145
|
+
history, so a surface left behind by an overtaken turn may also pass, until a
|
|
146
|
+
sweep such as `@haikit/postgres`'s `sweepOrphans` removes it. That is no
|
|
147
|
+
promise, just harmless: the handle only says where the browser shows the
|
|
148
|
+
notice, the model never sees it, and a notice can't change anything.
|
|
149
|
+
|
|
150
|
+
`notify` takes no lease, so it works while a turn is streaming, while one is
|
|
151
|
+
parked on a question, and with nobody connected. It never touches the history
|
|
152
|
+
itself: the next request that records a user message takes every unread notice
|
|
153
|
+
in. A typed message carries them ahead of what the user typed, and an answer to
|
|
154
|
+
a question carries them after its tool results. Taking them in commits with
|
|
155
|
+
that request's history, so a turn whose save is lost leaves them unread for
|
|
156
|
+
the next one. A notice never starts a turn.
|
|
157
|
+
|
|
158
|
+
With any notices listed, `hello` tells the browser to open a fourth route,
|
|
159
|
+
`GET /hai/events?conversationId=…`. It streams the conversation's notices,
|
|
160
|
+
each with its sequence number as its SSE `id`, so a browser that reconnects
|
|
161
|
+
with `Last-Event-ID` picks up where it left off. It takes no lease, writes
|
|
162
|
+
nothing, and accepts nothing from the browser but which conversation and where
|
|
163
|
+
to resume. The `model` text never goes out on it. The browser sees that only
|
|
164
|
+
once a turn has taken it in, as part of the history the `context` event shows
|
|
165
|
+
the inspector, the same as a digest. It reads the store every two
|
|
166
|
+
seconds, and a store with `watch` wakes it the moment a notice lands. The
|
|
167
|
+
two-second read stays even then, so a lost wake-up only delays a notice.
|
|
168
|
+
|
|
95
169
|
**A conversation closes once any of its surfaces passes its `staleAfterMs`.**
|
|
96
170
|
From then on both routes answer with an `expired` event and the model is not
|
|
97
171
|
called, so a picker left open over a weekend cannot resolve against last week's
|
package/dist/routes.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import type { Hai } from "./runtime.js";
|
|
3
3
|
/**
|
|
4
|
-
* Node http handler for the three routes
|
|
5
|
-
* request.
|
|
4
|
+
* Node http handler for the three routes, and a fourth when the app sends
|
|
5
|
+
* notices. Returns true if it handled the request.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* loop, start because the init tool it runs can render surfaces.
|
|
9
|
-
* interact route accepts: {handle, action, value} and nothing
|
|
10
|
-
* cannot name a tool, a handler, or an action target.
|
|
7
|
+
* The three POST routes stream SSE: chat and interact because both can resume
|
|
8
|
+
* the agent loop, start because the init tool it runs can render surfaces.
|
|
9
|
+
* Note what the interact route accepts: {handle, action, value} and nothing
|
|
10
|
+
* else. The client cannot name a tool, a handler, or an action target.
|
|
11
|
+
*
|
|
12
|
+
* `GET /events` streams a conversation's notices. It accepts nothing from the
|
|
13
|
+
* browser beyond which conversation, and where to resume.
|
|
11
14
|
*/
|
|
12
15
|
export declare function nodeHandler(hai: Hai, basePath?: string): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
13
16
|
//# sourceMappingURL=routes.d.ts.map
|
package/dist/routes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGjE,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAExC
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGjE,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAExC;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,SAAS,IACxB,KAAK,eAAe,EAAE,KAAK,cAAc,KAAG,OAAO,CAAC,OAAO,CAAC,CAuF1F"}
|
package/dist/routes.js
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
import { isConversationBusy, isStaleLease } from "@haikit/core";
|
|
2
2
|
/**
|
|
3
|
-
* Node http handler for the three routes
|
|
4
|
-
* request.
|
|
3
|
+
* Node http handler for the three routes, and a fourth when the app sends
|
|
4
|
+
* notices. Returns true if it handled the request.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* loop, start because the init tool it runs can render surfaces.
|
|
8
|
-
* interact route accepts: {handle, action, value} and nothing
|
|
9
|
-
* cannot name a tool, a handler, or an action target.
|
|
6
|
+
* The three POST routes stream SSE: chat and interact because both can resume
|
|
7
|
+
* the agent loop, start because the init tool it runs can render surfaces.
|
|
8
|
+
* Note what the interact route accepts: {handle, action, value} and nothing
|
|
9
|
+
* else. The client cannot name a tool, a handler, or an action target.
|
|
10
|
+
*
|
|
11
|
+
* `GET /events` streams a conversation's notices. It accepts nothing from the
|
|
12
|
+
* browser beyond which conversation, and where to resume.
|
|
10
13
|
*/
|
|
11
14
|
export function nodeHandler(hai, basePath = "/hai") {
|
|
12
15
|
return async function handle(req, res) {
|
|
13
16
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
14
|
-
if (
|
|
17
|
+
if (!url.pathname.startsWith(basePath))
|
|
15
18
|
return false;
|
|
16
19
|
const route = url.pathname.slice(basePath.length);
|
|
20
|
+
if (req.method === "GET" && route === "/events" && hai.sendsNotices) {
|
|
21
|
+
await streamNotices(hai, req, res, url);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
if (req.method !== "POST")
|
|
25
|
+
return false;
|
|
17
26
|
if (route !== "/chat" && route !== "/interact" && route !== "/start")
|
|
18
27
|
return false;
|
|
19
28
|
const body = await readJson(req);
|
|
@@ -40,7 +49,12 @@ export function nodeHandler(hai, basePath = "/hai") {
|
|
|
40
49
|
}
|
|
41
50
|
const emit = openSSE(res);
|
|
42
51
|
if (route === "/chat" || route === "/start") {
|
|
43
|
-
emit({
|
|
52
|
+
emit({
|
|
53
|
+
type: "hello",
|
|
54
|
+
conversationId: conversation.id,
|
|
55
|
+
model: hai.config.model.id,
|
|
56
|
+
...(hai.sendsNotices ? { events: true } : {}),
|
|
57
|
+
});
|
|
44
58
|
}
|
|
45
59
|
let superseded = false;
|
|
46
60
|
try {
|
|
@@ -87,6 +101,150 @@ export function nodeHandler(hai, basePath = "/hai") {
|
|
|
87
101
|
return true;
|
|
88
102
|
};
|
|
89
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* The longest a notice waits to be read. A store's `watch` makes it sooner;
|
|
106
|
+
* this is what still finds a notice whose wake-up was lost.
|
|
107
|
+
*/
|
|
108
|
+
const POLL_MS = 2_000;
|
|
109
|
+
/** Proxies drop a stream that says nothing for long; a comment keeps it open. */
|
|
110
|
+
const HEARTBEAT_MS = 25_000;
|
|
111
|
+
/** Notices read from the store at a time, so a long backlog goes out page by page. */
|
|
112
|
+
const PAGE = 100;
|
|
113
|
+
/**
|
|
114
|
+
* Stream a conversation's notices: every one after where the browser has got
|
|
115
|
+
* to (`Last-Event-ID` on a reconnect, else `?after=`), then each as it lands.
|
|
116
|
+
* Takes no lease and writes nothing. Each notice goes out as its payload; its
|
|
117
|
+
* model text is not in it: that reaches the browser only as part of the
|
|
118
|
+
* history, in the `context` event, once a turn has taken it in.
|
|
119
|
+
*
|
|
120
|
+
* Bounded both ways. The store is read a page at a time, so a conversation
|
|
121
|
+
* with years of notices is never held in memory at once, and nothing more is
|
|
122
|
+
* written while the socket is still taking what was, so a reader that is slow
|
|
123
|
+
* cannot make the server buffer every notice for it.
|
|
124
|
+
*/
|
|
125
|
+
async function streamNotices(hai, req, res, url) {
|
|
126
|
+
const { store } = hai.config;
|
|
127
|
+
const conversationId = url.searchParams.get("conversationId") ?? "";
|
|
128
|
+
const resume = Number(req.headers["last-event-id"] ?? url.searchParams.get("after") ?? 0);
|
|
129
|
+
let after = Number.isSafeInteger(resume) && resume > 0 ? resume : 0;
|
|
130
|
+
const first = await store.getNotices(conversationId, after, PAGE);
|
|
131
|
+
if (first === null) {
|
|
132
|
+
res.writeHead(404, { "content-type": "application/json" });
|
|
133
|
+
res.end(JSON.stringify({ error: "unknown conversation" }));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
res.writeHead(200, {
|
|
137
|
+
"content-type": "text/event-stream",
|
|
138
|
+
"cache-control": "no-cache",
|
|
139
|
+
connection: "keep-alive",
|
|
140
|
+
});
|
|
141
|
+
// Sent now, not with the first frame: with nothing to catch up on, that is a
|
|
142
|
+
// heartbeat away, and the browser's request would hang open until then.
|
|
143
|
+
res.flushHeaders();
|
|
144
|
+
const aborter = new AbortController();
|
|
145
|
+
const { signal } = aborter;
|
|
146
|
+
res.on("close", () => aborter.abort());
|
|
147
|
+
// Resolves once the socket has taken what is queued, or the browser has gone.
|
|
148
|
+
const write = async (chunk) => {
|
|
149
|
+
if (res.write(chunk))
|
|
150
|
+
return;
|
|
151
|
+
await new Promise((resolve) => {
|
|
152
|
+
const done = () => {
|
|
153
|
+
res.off("drain", done);
|
|
154
|
+
signal.removeEventListener("abort", done);
|
|
155
|
+
resolve();
|
|
156
|
+
};
|
|
157
|
+
res.on("drain", done);
|
|
158
|
+
signal.addEventListener("abort", done);
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
// Everything after `after`, a page at a time, until a page comes back short.
|
|
162
|
+
// A conversation deleted since the stream opened ends it: there is nothing
|
|
163
|
+
// more to wait for, and the browser's reconnect is then refused.
|
|
164
|
+
const catchUp = async (page = null) => {
|
|
165
|
+
for (;;) {
|
|
166
|
+
const records = page ?? (await store.getNotices(conversationId, after, PAGE));
|
|
167
|
+
if (records === null)
|
|
168
|
+
return aborter.abort();
|
|
169
|
+
for (const n of records) {
|
|
170
|
+
if (signal.aborted)
|
|
171
|
+
return;
|
|
172
|
+
if (n.seq <= after)
|
|
173
|
+
continue;
|
|
174
|
+
const event = {
|
|
175
|
+
type: "notice",
|
|
176
|
+
seq: n.seq,
|
|
177
|
+
name: n.name,
|
|
178
|
+
version: n.version,
|
|
179
|
+
payload: n.payload,
|
|
180
|
+
...(n.handle === undefined ? {} : { handle: n.handle }),
|
|
181
|
+
};
|
|
182
|
+
await write(`id: ${n.seq}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
183
|
+
after = n.seq;
|
|
184
|
+
}
|
|
185
|
+
if (records.length < PAGE || signal.aborted)
|
|
186
|
+
return;
|
|
187
|
+
page = null;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
// a heartbeat is only for a stream gone quiet, never one already backed up
|
|
191
|
+
const heartbeat = setInterval(() => res.writableNeedDrain || res.write(": ping\n\n"), HEARTBEAT_MS);
|
|
192
|
+
const wakes = wakeups(store.watch?.(conversationId, signal), signal)[Symbol.asyncIterator]();
|
|
193
|
+
try {
|
|
194
|
+
await catchUp(first);
|
|
195
|
+
// Watching starts before the next read, so a notice that landed after the
|
|
196
|
+
// backlog was read is either in that read or wakes the loop.
|
|
197
|
+
let next = wakes.next();
|
|
198
|
+
await catchUp();
|
|
199
|
+
while (!signal.aborted) {
|
|
200
|
+
if ((await next).done)
|
|
201
|
+
break;
|
|
202
|
+
next = wakes.next();
|
|
203
|
+
await catchUp();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
clearInterval(heartbeat);
|
|
208
|
+
aborter.abort();
|
|
209
|
+
await wakes.return?.();
|
|
210
|
+
res.end();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* When the events route reads next: every POLL_MS, and sooner whenever the
|
|
215
|
+
* store's `watch` says a notice may have landed. `watch` alone is not enough.
|
|
216
|
+
* A wake-up can be lost, and a LISTEN connection can drop, and either would
|
|
217
|
+
* leave the stream open on heartbeats with a notice it never reads. A `watch`
|
|
218
|
+
* that ends or fails leaves the reads every POLL_MS to carry on.
|
|
219
|
+
*/
|
|
220
|
+
async function* wakeups(watch, signal) {
|
|
221
|
+
const steps = watch?.[Symbol.asyncIterator]();
|
|
222
|
+
// started now, so the watch is listening before the caller's next read
|
|
223
|
+
let step = steps?.next().then((r) => !r.done, () => false) ?? null;
|
|
224
|
+
try {
|
|
225
|
+
while (!signal.aborted) {
|
|
226
|
+
let timer;
|
|
227
|
+
let onAbort;
|
|
228
|
+
const tick = new Promise((resolve) => {
|
|
229
|
+
onAbort = () => resolve("tick");
|
|
230
|
+
timer = setTimeout(onAbort, POLL_MS);
|
|
231
|
+
signal.addEventListener("abort", onAbort);
|
|
232
|
+
});
|
|
233
|
+
const woke = await Promise.race(step ? [tick, step] : [tick]);
|
|
234
|
+
clearTimeout(timer);
|
|
235
|
+
signal.removeEventListener("abort", onAbort);
|
|
236
|
+
if (signal.aborted)
|
|
237
|
+
return;
|
|
238
|
+
// the watch stepped: listen for the next; it ended or failed: stop listening
|
|
239
|
+
if (woke !== "tick")
|
|
240
|
+
step = woke ? steps.next().then((r) => !r.done, () => false) : null;
|
|
241
|
+
yield;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
await steps?.return?.();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
90
248
|
function openSSE(res) {
|
|
91
249
|
res.writeHead(200, {
|
|
92
250
|
"content-type": "text/event-stream",
|
package/dist/routes.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAIhE
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AnySurfaceImpl, type Conversation, type Emit, type ModelAdapter, type StoreAdapter, type Tool } from "@haikit/core";
|
|
1
|
+
import { type AnyNoticeImpl, type AnySurfaceImpl, type Conversation, type Emit, type ModelAdapter, type Notify, type StoreAdapter, type Tool } from "@haikit/core";
|
|
2
2
|
export interface HaiConfig {
|
|
3
3
|
model: ModelAdapter;
|
|
4
4
|
store: StoreAdapter;
|
|
@@ -33,6 +33,12 @@ export interface HaiConfig {
|
|
|
33
33
|
* model gets "already ran" back instead of running it again.
|
|
34
34
|
*/
|
|
35
35
|
init?: Tool;
|
|
36
|
+
/**
|
|
37
|
+
* The notices this app sends with `hai.notify`. A notice not listed here is
|
|
38
|
+
* refused, as an unregistered surface is. With any listed, the browser opens
|
|
39
|
+
* the events stream (`GET {base}/events`) to receive them.
|
|
40
|
+
*/
|
|
41
|
+
notices?: AnyNoticeImpl[];
|
|
36
42
|
}
|
|
37
43
|
/**
|
|
38
44
|
* Appended to every app's system prompt unless `scope` says otherwise. Generic
|
|
@@ -44,10 +50,29 @@ export declare class Hai {
|
|
|
44
50
|
readonly config: HaiConfig;
|
|
45
51
|
private readonly surfaces;
|
|
46
52
|
private readonly tools;
|
|
53
|
+
private readonly notices;
|
|
47
54
|
/** What the model gets as its system prompt: the app's, then the scope. */
|
|
48
55
|
readonly system: string;
|
|
49
56
|
private seq;
|
|
50
57
|
constructor(config: HaiConfig);
|
|
58
|
+
/** Whether this app sends notices, so the browser should open the events stream. */
|
|
59
|
+
get sendsNotices(): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Send a notice to a conversation: its payload to the browser now, over the
|
|
62
|
+
* events stream, and its model text to the model in the next user message
|
|
63
|
+
* the conversation records.
|
|
64
|
+
*
|
|
65
|
+
* Takes no lease, so it works while a turn is streaming, while one is
|
|
66
|
+
* parked, and with nobody connected. It never starts a turn.
|
|
67
|
+
*/
|
|
68
|
+
readonly notify: Notify;
|
|
69
|
+
/**
|
|
70
|
+
* The notices this conversation's history hasn't taken in yet: their model
|
|
71
|
+
* text as blocks for the user message this request is about to record, and
|
|
72
|
+
* the number to set `noticedThrough` to when it does. Set it only where the
|
|
73
|
+
* message is pushed, so a request refused before then takes nothing in.
|
|
74
|
+
*/
|
|
75
|
+
private unreadNotices;
|
|
51
76
|
private nid;
|
|
52
77
|
private buildQueryUiTool;
|
|
53
78
|
send(conversation: Conversation, text: string, emit: Emit): Promise<void>;
|
|
@@ -59,9 +84,10 @@ export declare class Hai {
|
|
|
59
84
|
*/
|
|
60
85
|
start(conversation: Conversation, emit: Emit): Promise<void>;
|
|
61
86
|
/**
|
|
62
|
-
* Run the init tool as the conversation's first tool call, after `
|
|
63
|
-
* the user's first message, or STARTED when it runs before one
|
|
64
|
-
*
|
|
87
|
+
* Run the init tool as the conversation's first tool call, after `message` —
|
|
88
|
+
* the user's first message, or STARTED when it runs before one, with any
|
|
89
|
+
* unread notices, which take the conversation's notices through `through`.
|
|
90
|
+
* Returns true when it parked the conversation on an elicit surface.
|
|
65
91
|
*
|
|
66
92
|
* Nothing reaches the history until the tool has succeeded — not even the
|
|
67
93
|
* user's message — so a refused start leaves the conversation exactly as
|
package/dist/runtime.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,aAAa,EAGlB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,IAAI,EACT,KAAK,YAAY,EACjB,KAAK,MAAM,EAEX,KAAK,YAAY,EACjB,KAAK,IAAI,EAIV,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,YAAY,CAAC;IACpB,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACvB,wCAAwC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;;;;;;;OAcG;IACH,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ;;;;OAIG;IACH,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;CAC3B;AASD;;;;GAIG;AACH,eAAO,MAAM,aAAa,ufAQ+B,CAAC;AAuB1D,qBAAa,GAAG;IACd,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqC;IAC9D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA2B;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoC;IAC5D,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,GAAG,CAAK;gBAEJ,MAAM,EAAE,SAAS;IA0B7B,oFAAoF;IACpF,IAAI,YAAY,IAAI,OAAO,CAE1B;IAED;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CA6BrB;IAEF;;;;;OAKG;YACW,aAAa;IAY3B,OAAO,CAAC,GAAG;IAMX,OAAO,CAAC,gBAAgB;IAkFlB,IAAI,CAAC,YAAY,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IA0C/E;;;;;OAKG;IACG,KAAK,CAAC,YAAY,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IASlE;;;;;;;;;OASG;YACW,OAAO;IAqEf,QAAQ,CACZ,YAAY,EAAE,YAAY,EAC1B,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,EACzD,IAAI,EAAE,IAAI,GACT,OAAO,CAAC,IAAI,CAAC;IAmHhB;;;;;;;;;;;;;;;;OAgBG;YACW,eAAe;YA4Cf,OAAO;YA0EP,UAAU;YA6NV,WAAW;CAa1B"}
|