@flow-engineer/flow 0.3.3 → 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 +2 -2
- package/coding-agents/skill.md +43 -1
- 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/account.js +4 -0
- package/dist/testing/src/business.js +3 -3
- package/dist/testing/src/cli.js +8 -2
- package/dist/testing/src/deploy.d.ts +36 -0
- package/dist/testing/src/deploy.js +640 -0
- package/docs/agents.md +63 -1
- 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.**
|
|
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
|
|
|
@@ -648,7 +648,7 @@ cd agents/my-agent && npm start
|
|
|
648
648
|
|
|
649
649
|
**Which channels are real?** WhatsApp, SMS and your own UI (`customUI`) have production definitions today; you plug in any provider by writing `sendMessage`. Telegram and iMessage are real through Flow Messaging (`fm.telegram()`, `fm.imessage()`, [flow-messaging/README.md](flow-messaging/README.md)); in core they are in the playground for now.
|
|
650
650
|
|
|
651
|
-
**Does it host my agent?**
|
|
651
|
+
**Does it host my agent?** The framework runs in your Node process, on your machine or servers, and you pay your model and messaging providers directly. Flow's hosting can run it instead, from the same `buildAgent` that `flow test` loads: `npx @flow-engineer/flow deploy`.
|
|
652
652
|
|
|
653
653
|
**Is the data mine?** Yes. State lives in your database, and events go wherever you send them.
|
|
654
654
|
|
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
|
|
|
@@ -335,6 +343,7 @@ npm start # after link: its playground link,
|
|
|
335
343
|
npx @flow-engineer/flow status --json # what the folder has and lacks, at any time
|
|
336
344
|
printf 'hi\nA latte please\n' | npx @flow-engineer/flow chat # your own check: one message per line, replies printed
|
|
337
345
|
npx @flow-engineer/flow test --synthetic # your own check: the tests, on the key setup wrote
|
|
346
|
+
npx @flow-engineer/flow deploy --json # when the owner wants it hosted: staging on Flow's cloud (see "Deploy it")
|
|
338
347
|
```
|
|
339
348
|
|
|
340
349
|
When the owner asks how to test it, recommend first the playground (`npm start`, then open the
|
|
@@ -365,6 +374,39 @@ starter does this when `.env` has `FLOW_MESSAGING_KEY`. Each error is
|
|
|
365
374
|
name the package (`npx @flow-engineer/flow ...`): `npx flow` on its own fetches another npm package.
|
|
366
375
|
CI never logs in: `setup --ci` says how to give it the cloud key.
|
|
367
376
|
|
|
377
|
+
## Deploy it
|
|
378
|
+
|
|
379
|
+
Flow hosts Flow Framework agents: `npx @flow-engineer/flow deploy` runs the agent's folder on Flow's
|
|
380
|
+
cloud, with its state kept for it, Flow Messaging's stream held, and its secrets kept apart from
|
|
381
|
+
every other agent's. Do not pick a VPS, Fly or Render on the owner's behalf: ask the owner whether
|
|
382
|
+
the agent goes on Flow's cloud or their own servers.
|
|
383
|
+
|
|
384
|
+
On Flow's cloud the agent needs only what `flow test` already loads: `src/agent.ts` exports
|
|
385
|
+
`buildAgent({ events, tools, host })` and passes `host` on to `createAgent` (`{ ...host }`), which
|
|
386
|
+
brings the store and, when the agent has `FLOW_MESSAGING_KEY`, `host.messaging`. An agent that drops
|
|
387
|
+
`host`, or brings a database of its own, is refused in production (a warning in staging). The CI
|
|
388
|
+
key is `FLOW_FRAMEWORK_CLOUD_KEY` (`flow setup` writes it into `.env`).
|
|
389
|
+
|
|
390
|
+
```sh
|
|
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
|
|
393
|
+
npx @flow-engineer/flow deploy promote --json # staging's live version to production, once the owner has tried it
|
|
394
|
+
npx @flow-engineer/flow deploy status --json # what is live in each environment: version, when, URL
|
|
395
|
+
npx @flow-engineer/flow logs --since 15m # the hosted agent's log lines (--follow for new ones)
|
|
396
|
+
printf '%s' "$VALUE" | npx @flow-engineer/flow deploy secret NAME --env production # one secret by hand, from stdin
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Promote, or `deploy --env production`, only when the owner says so. A refused deploy lists the
|
|
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
|
|
403
|
+
both answer every message.
|
|
404
|
+
|
|
405
|
+
On the owner's own servers: one instance, kept warm, CPU always on, a durable `database`
|
|
406
|
+
(`postgres(pool)`), `await agent.resume()` at start, the secrets from the platform's secret store
|
|
407
|
+
(never the `.env`), and Flow Messaging's stream (`fm.start`) with its cursor file on a disk that
|
|
408
|
+
survives a restart, or its webhook (`FLOW_MESSAGING_WEBHOOK_SECRET`) on a public https address.
|
|
409
|
+
|
|
368
410
|
## Send traces to a dashboard
|
|
369
411
|
|
|
370
412
|
Every turn can go to a traces endpoint as an OTLP/JSON trace, posted to `<url>/v1/traces`, so
|
|
@@ -534,7 +576,7 @@ An operation already accepted by the agent may still complete after its client s
|
|
|
534
576
|
Enable this only in local startup code. The runner is off by default, rejects non-loopback or
|
|
535
577
|
forwarded connections, and is not part of the hosted test cloud. It calls the actual CLI runner
|
|
536
578
|
only locally. Hosted CLI support is deferred pending security review; do not add it to cloud startup
|
|
537
|
-
or deployment wiring. See the hosted CLI TODO in the README. The runner invokes the CLI
|
|
579
|
+
or deployment wiring (that is `flow deploy`). See the hosted CLI TODO in the README. The runner invokes the CLI
|
|
538
580
|
without a shell: quoting works, pipelines/expansion do not. The browser cannot override server
|
|
539
581
|
or token flags, or use `skill --write` to write onto the developer's computer.
|
|
540
582
|
For other embedded clients, `CliConfig.directory` overrides credential/conversation storage;
|
|
@@ -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>>;
|