@flow-engineer/flow 0.3.4 → 0.4.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/README.md +1 -1
- package/coding-agents/skill.md +14 -5
- package/dist/src/business-events/engine.d.ts +316 -0
- package/dist/src/business-events/engine.js +547 -0
- package/dist/src/business-events/index.d.ts +5 -0
- package/dist/src/business-events/index.js +5 -0
- package/dist/src/business-events/phrasing.d.ts +13 -0
- package/dist/src/business-events/phrasing.js +33 -0
- package/dist/src/business-events/quiet.d.ts +8 -0
- package/dist/src/business-events/quiet.js +25 -0
- package/dist/src/business-events/signature.d.ts +8 -0
- package/dist/src/business-events/signature.js +36 -0
- package/dist/src/business-events/testlist.d.ts +15 -0
- package/dist/src/business-events/testlist.js +24 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/testing/src/business.js +3 -3
- package/dist/testing/src/deploy.d.ts +4 -2
- package/dist/testing/src/deploy.js +138 -33
- package/docs/agents.md +34 -9
- package/docs/events.md +243 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ Test it first in the playground (the link `npm start` prints: the test people an
|
|
|
50
50
|
|
|
51
51
|
**Stability.** 0.x: the CLI's commands, `--json` fields and exit codes in [docs/agents.md](docs/agents.md) stay compatible within 0.2.x; a breaking change comes with a minor version bump and a release note. Pin the version in `package.json` for production.
|
|
52
52
|
|
|
53
|
-
**Host it.** `npx @flow-engineer/flow deploy` puts the agent on Flow's hosting, so it keeps answering while the laptop sleeps: staging first, then `deploy promote` to production;
|
|
53
|
+
**Host it.** `npx @flow-engineer/flow deploy` puts the agent on Flow's hosting, so it keeps answering while the laptop sleeps: staging first, then `deploy promote` to production; it sends the `.env`'s keys as the agent's secrets itself. `deploy status` shows what is live and its URL, `flow logs` its log lines. It needs the agent's CI key (`FLOW_FRAMEWORK_CLOUD_KEY`, which `flow setup` writes) and a `buildAgent` that passes `options.host` on to `createAgent` ([docs/agents.md](docs/agents.md), "Host it").
|
|
54
54
|
|
|
55
55
|
### In a folder of your own
|
|
56
56
|
|
package/coding-agents/skill.md
CHANGED
|
@@ -226,6 +226,14 @@ before any send or turn (`queued`, or `refused`/`dropped`/`scheduled` decided th
|
|
|
226
226
|
`GET /nudges/<id>` reads its state. Both need `Authorization: Bearer <token>`. Contract:
|
|
227
227
|
`spec/nudges.openapi.yaml`.
|
|
228
228
|
|
|
229
|
+
When the backend posts what happened (an order shipped, an item is back in stock) rather than what
|
|
230
|
+
to send, mount `flowEvents({ secret, catalog, timezone })` (from `@flow-engineer/flow`): its
|
|
231
|
+
`routes` (`POST /v1/events`, signed with `Flow-Signature`), `onTick` and `context(conversationId)`
|
|
232
|
+
go on `createAgent`, `events.phrasing.check(address, outbound)` in the channel's send (throw its
|
|
233
|
+
reason to refuse), then `events.attach(agent)`. Each type in the catalog has a delivery level
|
|
234
|
+
(critical, standard, optional), its template and its values; the guide is `docs/events.md` in the
|
|
235
|
+
package (`../docs/events.md` from this file).
|
|
236
|
+
|
|
229
237
|
Templates are WhatsApp only. Declare the ones the owner set up with Meta beside `testPeople`, so
|
|
230
238
|
Flow Framework checks each send against them and the playground shows them as WhatsApp does:
|
|
231
239
|
|
|
@@ -380,17 +388,18 @@ brings the store and, when the agent has `FLOW_MESSAGING_KEY`, `host.messaging`.
|
|
|
380
388
|
key is `FLOW_FRAMEWORK_CLOUD_KEY` (`flow setup` writes it into `.env`).
|
|
381
389
|
|
|
382
390
|
```sh
|
|
383
|
-
npx @flow-engineer/flow deploy
|
|
384
|
-
|
|
391
|
+
npx @flow-engineer/flow deploy --json # staging, the first time too: sends .env's values as secrets
|
|
392
|
+
# (names printed, never values), builds, follows it to live; run its next
|
|
385
393
|
npx @flow-engineer/flow deploy promote --json # staging's live version to production, once the owner has tried it
|
|
386
394
|
npx @flow-engineer/flow deploy status --json # what is live in each environment: version, when, URL
|
|
387
395
|
npx @flow-engineer/flow logs --since 15m # the hosted agent's log lines (--follow for new ones)
|
|
388
|
-
printf '%s' "$VALUE" | npx @flow-engineer/flow deploy secret NAME --env production # one secret, from stdin
|
|
396
|
+
printf '%s' "$VALUE" | npx @flow-engineer/flow deploy secret NAME --env production # one secret by hand, from stdin
|
|
389
397
|
```
|
|
390
398
|
|
|
391
399
|
Promote, or `deploy --env production`, only when the owner says so. A refused deploy lists the
|
|
392
|
-
hosting lint's findings (`deploy.findings`): fix the errors and deploy again.
|
|
393
|
-
|
|
400
|
+
hosting lint's findings (`deploy.findings`): fix the errors and deploy again. One `deploy` is the whole first deploy: there is no
|
|
401
|
+
separate secrets step (it sends only new or changed `.env` values each time; promote sends them to
|
|
402
|
+
production). Commit first, or the version reads `<sha>+dirty`. Stop the laptop's `npm start` once the deployed one is live: two copies on one key
|
|
394
403
|
both answer every message.
|
|
395
404
|
|
|
396
405
|
On the owner's own servers: one instance, kept warm, CPU always on, a durable `database`
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import type { NudgeInput, NudgeResult } from "../agent.ts";
|
|
2
|
+
import type { Template } from "../types.ts";
|
|
3
|
+
import { type Phrasing } from "./phrasing.ts";
|
|
4
|
+
import { type EventQuietHours } from "./quiet.ts";
|
|
5
|
+
export type DeliveryLevel = "critical" | "standard" | "optional";
|
|
6
|
+
export declare const DELIVERY_LEVELS: readonly DeliveryLevel[];
|
|
7
|
+
/** The event cancelling one not yet sent: `data.cancels` is its id. */
|
|
8
|
+
export declare const CANCELLED_EVENT = "notification.cancelled";
|
|
9
|
+
/** One event, as the business posts it. */
|
|
10
|
+
export interface BusinessEvent {
|
|
11
|
+
/** Unique per event: the same id twice is processed once. */
|
|
12
|
+
id: string;
|
|
13
|
+
type: string;
|
|
14
|
+
occurredAt: string;
|
|
15
|
+
/** What it is about, as the business names it ("user:...", "order:..."). */
|
|
16
|
+
subject: string;
|
|
17
|
+
/** The floor: never sent at a lower level. Default: the type's in the catalog. */
|
|
18
|
+
delivery?: DeliveryLevel;
|
|
19
|
+
/** The business sent this message itself: kept as context, never sent. */
|
|
20
|
+
sentBy?: string;
|
|
21
|
+
/** Don't send after this (ISO time). Also read from `data.expiresAt`. */
|
|
22
|
+
expiresAt?: string;
|
|
23
|
+
/** A newer one with the same key replaces one still held. Also read from `data.collapseKey`. */
|
|
24
|
+
collapseKey?: string;
|
|
25
|
+
data: Record<string, any>;
|
|
26
|
+
}
|
|
27
|
+
/** A message ready either way: the template, its text as the person would see it, its values and its button link. */
|
|
28
|
+
export interface EventMessage {
|
|
29
|
+
template: Template;
|
|
30
|
+
text: string;
|
|
31
|
+
values: string[];
|
|
32
|
+
link: {
|
|
33
|
+
label: string;
|
|
34
|
+
url: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** A type that becomes a message for one person. */
|
|
38
|
+
export interface NotificationType {
|
|
39
|
+
kind?: "notification";
|
|
40
|
+
/** Its default level, when the event names none. */
|
|
41
|
+
level: DeliveryLevel;
|
|
42
|
+
/** Its message, or the field it lacks (a 400 names it). */
|
|
43
|
+
message(event: BusinessEvent): EventMessage | {
|
|
44
|
+
missing: string;
|
|
45
|
+
};
|
|
46
|
+
/** Its button acts on a live topic: dropped once the topic closes, whatever the level. */
|
|
47
|
+
liveOnly?: boolean;
|
|
48
|
+
/** What the message is about (an order, a product): how watches raise it, closing drops it, caps count it. */
|
|
49
|
+
topic?(event: BusinessEvent): string | undefined;
|
|
50
|
+
/** The topic's name, to find a watch when the event names no topic. */
|
|
51
|
+
title?(event: BusinessEvent): string | undefined;
|
|
52
|
+
}
|
|
53
|
+
/** A type the app handles itself in `onEvent` (no message of its own). */
|
|
54
|
+
export interface AppEventType {
|
|
55
|
+
kind: "event";
|
|
56
|
+
/** Why it is malformed, naming the field; null when fine. */
|
|
57
|
+
validate?(event: BusinessEvent): EventProblem | null;
|
|
58
|
+
}
|
|
59
|
+
export type EventType = NotificationType | AppEventType;
|
|
60
|
+
export type EventCatalog = Record<string, EventType>;
|
|
61
|
+
export interface EventProblem {
|
|
62
|
+
field: string;
|
|
63
|
+
error: string;
|
|
64
|
+
}
|
|
65
|
+
/** A send, held or about to go. */
|
|
66
|
+
export interface PendingSend {
|
|
67
|
+
eventId: string;
|
|
68
|
+
/** The event type it came from (a nudge: `nudge.<kind>`), for logs. */
|
|
69
|
+
type?: string;
|
|
70
|
+
address: string;
|
|
71
|
+
delivery: DeliveryLevel | "nudge";
|
|
72
|
+
/** Raised by a watch: "watch" is kept, "urgent" also goes at once. */
|
|
73
|
+
raised?: "watch" | "urgent";
|
|
74
|
+
/** Never dropped as stale, whatever closes. */
|
|
75
|
+
sticky?: true;
|
|
76
|
+
/** Dropped when its topic closes, whatever the level. */
|
|
77
|
+
liveOnly?: true;
|
|
78
|
+
/** A nudge's kind, as the app names it. */
|
|
79
|
+
nudge?: string;
|
|
80
|
+
topic?: string;
|
|
81
|
+
collapseKey?: string;
|
|
82
|
+
notBefore: number;
|
|
83
|
+
expiresAt?: number;
|
|
84
|
+
/** The template, never the agent's words. */
|
|
85
|
+
exact: boolean;
|
|
86
|
+
message: EventMessage;
|
|
87
|
+
}
|
|
88
|
+
/** What a person asked to be told about. Apps add their own fields. */
|
|
89
|
+
export interface Watch {
|
|
90
|
+
id: string;
|
|
91
|
+
address: string;
|
|
92
|
+
/** Only watches with a topic raise messages. */
|
|
93
|
+
topic?: string;
|
|
94
|
+
/** The topic's name, as the agent saw it. */
|
|
95
|
+
title?: string;
|
|
96
|
+
/** "As soon as": raised messages go at once, even in quiet hours. */
|
|
97
|
+
urgent?: true;
|
|
98
|
+
createdAt: number;
|
|
99
|
+
/** When its topic closed. */
|
|
100
|
+
endedAt?: number;
|
|
101
|
+
}
|
|
102
|
+
/** A message the business sent itself (`sentBy`), kept as context. */
|
|
103
|
+
export interface EventCopy {
|
|
104
|
+
eventId: string;
|
|
105
|
+
type: string;
|
|
106
|
+
/** When it was sent (the envelope's occurredAt), ms. */
|
|
107
|
+
at: number;
|
|
108
|
+
text: string;
|
|
109
|
+
values: string[];
|
|
110
|
+
link: {
|
|
111
|
+
label: string;
|
|
112
|
+
url: string;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export interface SentNudge {
|
|
116
|
+
address: string;
|
|
117
|
+
topic?: string;
|
|
118
|
+
kind?: string;
|
|
119
|
+
at: number;
|
|
120
|
+
}
|
|
121
|
+
/** What the events side keeps. Pass your own (with extra fields of your app's) to share it. */
|
|
122
|
+
export declare function createEventState<W extends Watch = Watch>(): {
|
|
123
|
+
seen: Set<string>;
|
|
124
|
+
cancelled: Set<string>;
|
|
125
|
+
outbox: PendingSend[];
|
|
126
|
+
watches: W[];
|
|
127
|
+
/** Closed topics. */
|
|
128
|
+
closed: Set<string>;
|
|
129
|
+
/** `${address} ${topic}`: no more nudges about it. */
|
|
130
|
+
muted: Set<string>;
|
|
131
|
+
nudges: SentNudge[];
|
|
132
|
+
/** `${address} ${collapseKey|eventId}` -> when an optional last went. */
|
|
133
|
+
optionalLast: Map<string, number>;
|
|
134
|
+
/** The last nudged topic per address, for "not interested". */
|
|
135
|
+
lastNudged: Map<string, string>;
|
|
136
|
+
/** Address -> copies the business sent itself, newest last. */
|
|
137
|
+
copies: Map<string, EventCopy[]>;
|
|
138
|
+
/** `${address}\n${text}` -> when it reached them: the same message never goes again within a day. */
|
|
139
|
+
told: Map<string, number>;
|
|
140
|
+
/** Conversation id -> address. */
|
|
141
|
+
addresses: Map<string, string>;
|
|
142
|
+
};
|
|
143
|
+
export type EventState<W extends Watch = Watch> = ReturnType<typeof createEventState<W>>;
|
|
144
|
+
/** What sends: the agent (its nudge, and reach for copies). */
|
|
145
|
+
export interface EventSender {
|
|
146
|
+
nudge(input: NudgeInput): Promise<NudgeResult>;
|
|
147
|
+
reach?(to: {
|
|
148
|
+
channel: string;
|
|
149
|
+
address: string;
|
|
150
|
+
}): Promise<{
|
|
151
|
+
conversationId: string;
|
|
152
|
+
}>;
|
|
153
|
+
}
|
|
154
|
+
export interface FlowEventsOptions<W extends Watch = Watch> {
|
|
155
|
+
/** The whsec_ secret the business signs with; without it every POST is refused (503). */
|
|
156
|
+
secret?: string;
|
|
157
|
+
/** Every type accepted (plus notification.cancelled); any other is a 400. */
|
|
158
|
+
catalog: EventCatalog;
|
|
159
|
+
now?: () => number;
|
|
160
|
+
/** The channel messages go on. Default whatsapp. */
|
|
161
|
+
channel?: string;
|
|
162
|
+
/** The zone quiet hours and copies' times are read in. Default UTC. */
|
|
163
|
+
timezone?: string;
|
|
164
|
+
/** How the zone reads in log reasons and notes ("Pacific time"). Default the zone's name. */
|
|
165
|
+
timezoneName?: string;
|
|
166
|
+
/** Default 22:00 to 08:00; null: none. */
|
|
167
|
+
quietHours?: EventQuietHours | null;
|
|
168
|
+
/** Nudge caps (raised nudges skip them). Default 2 a day, 1 per topic a day, 5 a week. */
|
|
169
|
+
caps?: {
|
|
170
|
+
perDay: number;
|
|
171
|
+
perKeyPerDay: number;
|
|
172
|
+
perWeek: number;
|
|
173
|
+
};
|
|
174
|
+
/** How long a watch lasts. Default 30. */
|
|
175
|
+
watchDays?: number;
|
|
176
|
+
state?: EventState<W>;
|
|
177
|
+
/** Who may post copies (`sentBy`); none: sentBy is refused. */
|
|
178
|
+
sentBy?: string | string[];
|
|
179
|
+
/** The business's name in log reasons ("Acme"). Default the first sentBy, else "the business". */
|
|
180
|
+
businessName?: string;
|
|
181
|
+
/** Off production, who a business-started send may go to (eventTestList). */
|
|
182
|
+
testList?: {
|
|
183
|
+
allows(address: string): boolean;
|
|
184
|
+
};
|
|
185
|
+
/** A send the test list stopped. */
|
|
186
|
+
onUnlisted?(send: {
|
|
187
|
+
event: string;
|
|
188
|
+
type: string;
|
|
189
|
+
address: string;
|
|
190
|
+
}): void;
|
|
191
|
+
/** Each decision as fields (ids, types, outcomes; never text). Default one JSON line, numbers masked. */
|
|
192
|
+
log?(fields: Record<string, unknown>): void;
|
|
193
|
+
/** The default log line's msg. Default "flow event". */
|
|
194
|
+
logName?: string;
|
|
195
|
+
/** What a topic is called in log reasons ("order"). Default "topic". */
|
|
196
|
+
topicNoun?: string;
|
|
197
|
+
/** The log reason for a muted nudge. Default "muted (not interested)". */
|
|
198
|
+
mutedReason?: string;
|
|
199
|
+
/** Where a notification's person is. Default data.user.phone. */
|
|
200
|
+
addressOf?(event: BusinessEvent): string | undefined;
|
|
201
|
+
/** Whether they agreed to messages. Default data.consent.whatsappUpdates === true. */
|
|
202
|
+
consented?(event: BusinessEvent): boolean;
|
|
203
|
+
wording?: {
|
|
204
|
+
/** The note the agent gets to phrase a message (nudge: one the business started from a watch). */
|
|
205
|
+
prompt?(message: EventMessage, options: {
|
|
206
|
+
nudge: boolean;
|
|
207
|
+
}): string;
|
|
208
|
+
/** The stop line a nudge sent as text ends with. */
|
|
209
|
+
stopLine?: string;
|
|
210
|
+
/** The text fallback puts the link in the text instead of a link button. */
|
|
211
|
+
linkInText?: boolean;
|
|
212
|
+
};
|
|
213
|
+
copies?: {
|
|
214
|
+
kept?: number;
|
|
215
|
+
shown?: number;
|
|
216
|
+
days?: number;
|
|
217
|
+
/** The agent's note on recent copies; "" for none. */
|
|
218
|
+
note?(copies: EventCopy[], options: {
|
|
219
|
+
timezone: string;
|
|
220
|
+
timezoneName: string;
|
|
221
|
+
}): string;
|
|
222
|
+
};
|
|
223
|
+
rules?: {
|
|
224
|
+
/** Before the raise: "handled" when the app sent (or dropped) it itself; else it goes on. */
|
|
225
|
+
notification?(event: BusinessEvent, send: PendingSend, events: FlowEvents<W>): "send" | "handled" | Promise<"send" | "handled">;
|
|
226
|
+
/** A copy arrived: the app's own bookkeeping, and held sends it also counts as told. */
|
|
227
|
+
copy?(event: BusinessEvent, message: EventMessage, events: FlowEvents<W>): ((held: PendingSend) => boolean) | void;
|
|
228
|
+
};
|
|
229
|
+
/** Types of kind "event", in order with the rest. */
|
|
230
|
+
onEvent?(event: BusinessEvent, events: FlowEvents<W>): Promise<void> | void;
|
|
231
|
+
/** After held sends came due, in order with the rest. */
|
|
232
|
+
onTick?(now: number, events: FlowEvents<W>): Promise<void> | void;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* The events side of an agent: mount `routes` and `onTick` on createAgent, `phrasing.check` in the
|
|
236
|
+
* channel's send, `context` in the agent's context, then `attach(agent)`.
|
|
237
|
+
*/
|
|
238
|
+
export declare function flowEvents<W extends Watch = Watch>(options: FlowEventsOptions<W>): {
|
|
239
|
+
state: {
|
|
240
|
+
seen: Set<string>;
|
|
241
|
+
cancelled: Set<string>;
|
|
242
|
+
outbox: PendingSend[];
|
|
243
|
+
watches: W[];
|
|
244
|
+
/** Closed topics. */
|
|
245
|
+
closed: Set<string>;
|
|
246
|
+
/** `${address} ${topic}`: no more nudges about it. */
|
|
247
|
+
muted: Set<string>;
|
|
248
|
+
nudges: SentNudge[];
|
|
249
|
+
/** `${address} ${collapseKey|eventId}` -> when an optional last went. */
|
|
250
|
+
optionalLast: Map<string, number>;
|
|
251
|
+
/** The last nudged topic per address, for "not interested". */
|
|
252
|
+
lastNudged: Map<string, string>;
|
|
253
|
+
/** Address -> copies the business sent itself, newest last. */
|
|
254
|
+
copies: Map<string, EventCopy[]>;
|
|
255
|
+
/** `${address}\n${text}` -> when it reached them: the same message never goes again within a day. */
|
|
256
|
+
told: Map<string, number>;
|
|
257
|
+
/** Conversation id -> address. */
|
|
258
|
+
addresses: Map<string, string>;
|
|
259
|
+
};
|
|
260
|
+
phrasing: Phrasing;
|
|
261
|
+
handler: (req: Request) => Promise<Response>;
|
|
262
|
+
/** For createAgent's `routes`. */
|
|
263
|
+
routes: Record<string, (request: Request) => Promise<Response>>;
|
|
264
|
+
/** For createAgent's `onTick`. */
|
|
265
|
+
onTick: () => Promise<undefined>;
|
|
266
|
+
attach(s: EventSender): void;
|
|
267
|
+
invalid: (body: unknown) => EventProblem | null;
|
|
268
|
+
accept: (e: BusinessEvent) => boolean;
|
|
269
|
+
tick: () => Promise<unknown>;
|
|
270
|
+
/** Resolves once everything accepted so far is processed. */
|
|
271
|
+
idle: () => Promise<undefined>;
|
|
272
|
+
report: (fields: Record<string, unknown>) => void;
|
|
273
|
+
schedule: (p: PendingSend, exempt?: boolean) => Promise<void>;
|
|
274
|
+
drop: (match: (p: PendingSend) => boolean) => number;
|
|
275
|
+
raise: (event: string, type: string, delivery: DeliveryLevel, watch: Watch | undefined) => {
|
|
276
|
+
delivery: DeliveryLevel;
|
|
277
|
+
raised?: "watch" | "urgent";
|
|
278
|
+
};
|
|
279
|
+
/** This person's live watch on a topic (by topic, else by its name, with `titleOf` the app's names for topics). */
|
|
280
|
+
watchFor: (address: string, topic: string | undefined, title?: unknown) => W | undefined;
|
|
281
|
+
/** How a topic's name is found, for watches matched by name. */
|
|
282
|
+
setTitles(fn: (topic: string) => string | undefined): void;
|
|
283
|
+
/** Whether a watch is within its days. */
|
|
284
|
+
alive: (w: Watch, t?: number) => boolean;
|
|
285
|
+
told: (address: string, text: string) => boolean;
|
|
286
|
+
muted: (address: string, topic: string | undefined) => boolean;
|
|
287
|
+
isCopy: (e: BusinessEvent) => boolean;
|
|
288
|
+
/**
|
|
289
|
+
* Saves a watch; `same` finds one this person already has (it is refreshed: another 30 days, its
|
|
290
|
+
* title kept, urgent kept). "saved" or "already watching".
|
|
291
|
+
*/
|
|
292
|
+
watch(w: Omit<W, "id" | "createdAt"> & {
|
|
293
|
+
createdAt?: number;
|
|
294
|
+
}, same?: (existing: W) => boolean): {
|
|
295
|
+
outcome: "saved" | "already watching";
|
|
296
|
+
watch: W;
|
|
297
|
+
};
|
|
298
|
+
/**
|
|
299
|
+
* A topic closed (sold, cancelled, ended): held sends about it are dropped, except a critical or
|
|
300
|
+
* raised one, a sticky one, and never a live-only one; its watches end. How many were dropped.
|
|
301
|
+
*/
|
|
302
|
+
close(topic: string): number;
|
|
303
|
+
/** No more nudges to this person about this topic. */
|
|
304
|
+
mute(address: string, topic: string): void;
|
|
305
|
+
/** "Not interested" on a nudge: that topic is muted, its watch and held nudges go. The topic, or null. */
|
|
306
|
+
notInterested(address: string): string | null;
|
|
307
|
+
/** STOP: their watches end and held nudges go (the agent itself stops every send). */
|
|
308
|
+
stopped(address: string): void;
|
|
309
|
+
/** Which address a conversation is (from tool calls, copies). */
|
|
310
|
+
remember(conversationId: string, address: string): void;
|
|
311
|
+
/** The note on recent copies for this person; "" when none. */
|
|
312
|
+
copiesNote(address: string): string;
|
|
313
|
+
/** The turn's context: recent copies for this conversation's person ("" when none). `address` when the caller knows it. */
|
|
314
|
+
context(conversationId: string, address?: string | null): string;
|
|
315
|
+
};
|
|
316
|
+
export type FlowEvents<W extends Watch = Watch> = ReturnType<typeof flowEvents<W>>;
|