@stratusagent/core 0.13.6 → 0.13.7
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/dist/channels.d.ts +661 -0
- package/dist/channels.d.ts.map +1 -0
- package/dist/channels.js +31 -0
- package/dist/channels.js.map +1 -0
- package/dist/executors.d.ts +35 -0
- package/dist/executors.d.ts.map +1 -0
- package/dist/executors.js +64 -0
- package/dist/executors.js.map +1 -0
- package/dist/index.d.ts +211 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +236 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
The Stratus Agent kernel: contracts for sessions, providers, tools, agents, memory, credentials, and events, plus a minimal multi-turn orchestration runtime (`AgentRunner`) with in-memory defaults.
|
|
4
4
|
|
|
5
|
-
Providers, executors, transports, and UIs live in separate packages — core stays small, dependency-light, and platform-agnostic.
|
|
5
|
+
Providers, executors, transports, and UIs live in separate packages — core stays small, dependency-light, and platform-agnostic. What those packages build against is here: the channel contract a transport implements (`ChannelAdapter`, `channelSessionKey`, the sender policy) and the executor helpers (`successResult`, `failureResult`, `createDirectExecutor`), formerly the separate packages `@stratusagent/channels` and `@stratusagent/executors`.
|
|
6
6
|
|
|
7
7
|
See the [repository README](https://github.com/stratuslabs/agent) for the full picture.
|
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import type { AgentDefinition, ApprovalAnswer, CredentialScope, EventBus, ImageAttachment, JsonObject, Session, TrustLevel } from './index.ts';
|
|
2
|
+
/**
|
|
3
|
+
* One message arriving from a chat platform, normalized. Adapters translate
|
|
4
|
+
* their platform's event shape into this before anything else happens.
|
|
5
|
+
*/
|
|
6
|
+
export interface InboundMessage {
|
|
7
|
+
/** Channel kind, e.g. 'slack'. */
|
|
8
|
+
channel: string;
|
|
9
|
+
/** Workspace / team identifier. */
|
|
10
|
+
team: string;
|
|
11
|
+
/** The conversation container (Slack: channel id). */
|
|
12
|
+
conversation: string;
|
|
13
|
+
/**
|
|
14
|
+
* Thread identifier within the conversation, when the platform has
|
|
15
|
+
* threads. For a Slack top-level message this is the message's own ts —
|
|
16
|
+
* its reply thread becomes the conversation.
|
|
17
|
+
*/
|
|
18
|
+
thread?: string;
|
|
19
|
+
author: {
|
|
20
|
+
id: string;
|
|
21
|
+
displayName?: string;
|
|
22
|
+
};
|
|
23
|
+
/** Message text with any bot-mention markup already stripped. */
|
|
24
|
+
text: string;
|
|
25
|
+
/** Whether the agent was explicitly addressed (mention or DM). */
|
|
26
|
+
mentionsAgent: boolean;
|
|
27
|
+
/** Platform event id, used to dedupe redeliveries. */
|
|
28
|
+
eventId: string;
|
|
29
|
+
}
|
|
30
|
+
/** An outbound message handle the adapter can keep editing. */
|
|
31
|
+
export interface OutboundMessageRef {
|
|
32
|
+
channel: string;
|
|
33
|
+
ts: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The write side of one conversation. `post` is the whole of what every
|
|
37
|
+
* caller needs: the gateway's `message.send` and schedule delivery only ever
|
|
38
|
+
* post. Everything else is a capability a platform may not have, so it is
|
|
39
|
+
* optional, and a caller that wants it checks for the method rather than
|
|
40
|
+
* assuming it — the way `ChannelAdapter.resolveOutbound` works.
|
|
41
|
+
*
|
|
42
|
+
* Streaming by placeholder-then-edit (post once, edit as fragments arrive,
|
|
43
|
+
* finalize with the full text) is how a platform with editable messages
|
|
44
|
+
* shows a reply forming. A text-message channel has no such thing — an
|
|
45
|
+
* iMessage edit is visibly marked and capped — so it posts the finished
|
|
46
|
+
* reply instead.
|
|
47
|
+
*/
|
|
48
|
+
export interface OutboundConnection {
|
|
49
|
+
post(text: string): Promise<OutboundMessageRef>;
|
|
50
|
+
/**
|
|
51
|
+
* Rewrites a message this connection posted. A channel without it gives
|
|
52
|
+
* up streaming a reply in place: it posts the final text once.
|
|
53
|
+
*/
|
|
54
|
+
edit?(ref: OutboundMessageRef, text: string): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Uploads a local file into the conversation (tool outputs, screenshots).
|
|
57
|
+
* A channel without it cannot deliver files over an addressable
|
|
58
|
+
* destination; a caller says so instead of dropping the file silently.
|
|
59
|
+
*/
|
|
60
|
+
upload?(filePath: string, title?: string): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Optional typing affordance. Platforms without a real bot typing API
|
|
63
|
+
* (Slack among them) no-op here and rely on the placeholder-edit pattern
|
|
64
|
+
* to show liveness.
|
|
65
|
+
*/
|
|
66
|
+
typing?(): Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The slice of the gateway a channel adapter may touch. Adapters translate
|
|
70
|
+
* events only — they dispatch inbound messages and render the event stream
|
|
71
|
+
* back out. Providers, tools, and memory are never theirs to reach; if an
|
|
72
|
+
* adapter needs more, the fix belongs in the gateway.
|
|
73
|
+
*/
|
|
74
|
+
/**
|
|
75
|
+
* Where a session came from, for a channel that has to reach it with no
|
|
76
|
+
* live turn to hang the message on.
|
|
77
|
+
*
|
|
78
|
+
* Deliberately not the session. The routing keys are what the channel
|
|
79
|
+
* itself wrote at dispatch, and reading those back is a different
|
|
80
|
+
* capability from reading the conversation — an adapter that needs the
|
|
81
|
+
* transcript has stopped translating events and become something else.
|
|
82
|
+
*/
|
|
83
|
+
export interface SessionRouting {
|
|
84
|
+
/** Whose app must do the talking. */
|
|
85
|
+
agentId: string;
|
|
86
|
+
/** The metadata the dispatching channel attached to the session. */
|
|
87
|
+
metadata: JsonObject;
|
|
88
|
+
/**
|
|
89
|
+
* When this agent last SPOKE in the conversation, ISO-8601 — the
|
|
90
|
+
* timestamp of its newest reply, absent when it has never produced one.
|
|
91
|
+
*
|
|
92
|
+
* Deliberately not the session's modification time, which moves on every
|
|
93
|
+
* save a turn makes: a tool result, an approval checkpoint, a recovery
|
|
94
|
+
* resuming after a restart. An agent part-way through a long turn would
|
|
95
|
+
* look like the most recent speaker under that reading while another
|
|
96
|
+
* agent had actually answered, and the Slack adapter orders the agents
|
|
97
|
+
* sharing a thread by this to decide whose an untagged follow-up is.
|
|
98
|
+
*
|
|
99
|
+
* A file counts: a turn whose tool result carried one (`filePathsOf` in
|
|
100
|
+
* `@stratusagent/core`) put something in the thread, whether or not it
|
|
101
|
+
* said a word beside it.
|
|
102
|
+
*
|
|
103
|
+
* Still routing, not transcript: it says *when* an agent spoke, never
|
|
104
|
+
* what was said. Optional because a host may not track it; a caller that
|
|
105
|
+
* needs to order two sessions checks for it rather than assuming, and
|
|
106
|
+
* says what it gives up without it.
|
|
107
|
+
*/
|
|
108
|
+
lastSpokeAt?: string;
|
|
109
|
+
/**
|
|
110
|
+
* The text the session's latest turn produced (`latestTurnReply` in
|
|
111
|
+
* `@stratusagent/core`, the same rule an adapter finalizes its own turns
|
|
112
|
+
* by; absent when the turn produced none) — for a turn the adapter did
|
|
113
|
+
* not start and so never rendered. A turn
|
|
114
|
+
* parked on a human when the daemon died is re-asked after the restart
|
|
115
|
+
* and finishes in a process that has no placeholder to edit; without
|
|
116
|
+
* this, the approval survived the restart and the reply went nowhere.
|
|
117
|
+
*/
|
|
118
|
+
reply?: string;
|
|
119
|
+
/**
|
|
120
|
+
* Whether the turn the session is on is one nobody asked for — its
|
|
121
|
+
* newest user message was dispatched `addressed: false`
|
|
122
|
+
* (`isUnaddressedTurn` in `@stratusagent/core`). Read for a turn the
|
|
123
|
+
* adapter did not render: a judged turn the daemon died inside is
|
|
124
|
+
* failed at the next start by a process with no renderer for it, and
|
|
125
|
+
* an error note posted for a turn nobody asked for, that said nothing,
|
|
126
|
+
* is the interruption the turn existed to avoid.
|
|
127
|
+
*/
|
|
128
|
+
unaddressed?: boolean;
|
|
129
|
+
}
|
|
130
|
+
export interface GatewayLike {
|
|
131
|
+
dispatch(input: {
|
|
132
|
+
sessionId: string;
|
|
133
|
+
agentId?: string;
|
|
134
|
+
userMessage: string;
|
|
135
|
+
/** Images sent with the message — see `Message.images` in `@stratusagent/core`. */
|
|
136
|
+
images?: ImageAttachment[];
|
|
137
|
+
/**
|
|
138
|
+
* Whether the message was said to the agent — see `RunInput.addressed`
|
|
139
|
+
* in `@stratusagent/core`. `false` runs a turn the agent may answer
|
|
140
|
+
* with nothing: the session completes with no reply, and the adapter
|
|
141
|
+
* posts nothing for it.
|
|
142
|
+
*/
|
|
143
|
+
addressed?: boolean;
|
|
144
|
+
/**
|
|
145
|
+
* What was said in the conversation before this message that the agent
|
|
146
|
+
* never heard — a thread it is mentioned into partway — oldest first,
|
|
147
|
+
* each entry with its own metadata for its speaker's trust. Used only
|
|
148
|
+
* when the dispatch creates the session; see `RunInput.earlier` in
|
|
149
|
+
* `@stratusagent/core`. A host without it ignores the field, and the
|
|
150
|
+
* agent sees only the message that named it.
|
|
151
|
+
*/
|
|
152
|
+
earlier?: Array<{
|
|
153
|
+
message: string;
|
|
154
|
+
metadata?: JsonObject;
|
|
155
|
+
}>;
|
|
156
|
+
metadata?: JsonObject;
|
|
157
|
+
signal?: AbortSignal;
|
|
158
|
+
/**
|
|
159
|
+
* Caller-chosen id for this turn, so the adapter can tell its own turn's
|
|
160
|
+
* events from another caller's on the same session — see `activeTurnId`.
|
|
161
|
+
*/
|
|
162
|
+
turnId?: string;
|
|
163
|
+
/**
|
|
164
|
+
* The platform's own id for the message, unique within the session, so
|
|
165
|
+
* a redelivery never starts a second turn — across a restart too, which
|
|
166
|
+
* in-memory dedupe cannot survive. A repeat resolves with the original
|
|
167
|
+
* turn: the live one, the finished session, or one the gateway continued
|
|
168
|
+
* after a crash. A finished session may have moved on since, so the
|
|
169
|
+
* reply to post is `turnReplyFor(session, key)` from
|
|
170
|
+
* `@stratusagent/core`, never the latest one. See `DispatchInput.idempotencyKey` in
|
|
171
|
+
* `@stratusagent/gateway`.
|
|
172
|
+
*
|
|
173
|
+
* A host without it ignores the field, and the adapter's own dedupe is
|
|
174
|
+
* all there is.
|
|
175
|
+
*/
|
|
176
|
+
idempotencyKey?: string;
|
|
177
|
+
/**
|
|
178
|
+
* Called before the dispatch resolves when it repeats a turn. `live`:
|
|
179
|
+
* another dispatch of the same key in this process is still waiting on
|
|
180
|
+
* it and posts its outcome, so this caller takes down anything of its
|
|
181
|
+
* own and posts nothing. `finished`: nothing ran and no event will
|
|
182
|
+
* come, and the outcome may never have been posted, so this caller
|
|
183
|
+
* posts it from the session — `turnReplyFor`, `turnFilesFor`,
|
|
184
|
+
* `turnFailureFor` in `@stratusagent/core`. Not called for a repeat
|
|
185
|
+
* that continues a turn a crash left unfinished, which this caller
|
|
186
|
+
* renders as its own. See `DispatchInput.onRepeat` in
|
|
187
|
+
* `@stratusagent/gateway`.
|
|
188
|
+
*/
|
|
189
|
+
onRepeat?: (repeat: 'live' | 'finished') => void;
|
|
190
|
+
/**
|
|
191
|
+
* Opts into merged turns: sent while the session's turn runs, this
|
|
192
|
+
* message may be answered by one next turn together with others that
|
|
193
|
+
* queued. Called before the dispatch settles when it was absorbed into
|
|
194
|
+
* such a turn led by an earlier message, whose caller posts the answer,
|
|
195
|
+
* so this one takes down anything of its own and posts nothing. A host
|
|
196
|
+
* without it ignores the field. See `DispatchInput.onAbsorbed` in
|
|
197
|
+
* `@stratusagent/gateway`.
|
|
198
|
+
*/
|
|
199
|
+
onAbsorbed?: () => void;
|
|
200
|
+
}): Promise<Session>;
|
|
201
|
+
readonly bus: EventBus;
|
|
202
|
+
agents(): AgentDefinition[];
|
|
203
|
+
/**
|
|
204
|
+
* The `turnId` of the turn currently running on a session, if its caller
|
|
205
|
+
* named one. Optional: a host that omits it leaves the adapter to guess
|
|
206
|
+
* which turn an outcome belongs to from the order events arrive in, which
|
|
207
|
+
* is right for every turn the adapter started itself and wrong for a turn
|
|
208
|
+
* another surface dispatched to the same session while a message of the
|
|
209
|
+
* adapter's was queued behind it.
|
|
210
|
+
*/
|
|
211
|
+
activeTurnId?(sessionId: string): string | undefined;
|
|
212
|
+
/**
|
|
213
|
+
* Aborts every turn running on a session, including one the host
|
|
214
|
+
* recovered after a restart that no adapter started, and returns how many
|
|
215
|
+
* turns are on the session, each now stopping (one already aborted that
|
|
216
|
+
* has not settled still counts; 0 means nothing is running there).
|
|
217
|
+
* Optional: a host without it leaves
|
|
218
|
+
* an adapter able to stop only the turns it dispatched with its own
|
|
219
|
+
* signal. The adapter decides who may stop; the host asks nothing.
|
|
220
|
+
*/
|
|
221
|
+
abortTurns?(sessionId: string, reason: string): number;
|
|
222
|
+
/**
|
|
223
|
+
* Where a durable session came from, or undefined if there is no such
|
|
224
|
+
* session.
|
|
225
|
+
*
|
|
226
|
+
* Optional for the same reason `SessionStore.listIdsByStatus` is: a
|
|
227
|
+
* gateway that cannot answer simply cannot be asked, and a caller that
|
|
228
|
+
* needs it says so by checking for the method rather than assuming one
|
|
229
|
+
* exists. An adapter without it loses the ability to speak about a turn
|
|
230
|
+
* whose process is gone — nothing else.
|
|
231
|
+
*/
|
|
232
|
+
sessionRouting?(sessionId: string): Promise<SessionRouting | undefined>;
|
|
233
|
+
/**
|
|
234
|
+
* Whether a session already accepted the message an idempotency key names,
|
|
235
|
+
* as addressed to it — a turn was started for it, finished or not; a turn
|
|
236
|
+
* nobody asked for (`addressed: false`) does not count. Asked before routing a message
|
|
237
|
+
* that more than one agent could answer: one an agent already accepted is
|
|
238
|
+
* that agent's, whoever the routing rule would pick now. Optional: a host
|
|
239
|
+
* without it leaves the choice to the rule alone, which after a restart can
|
|
240
|
+
* hand a redelivered message to an agent that has spoken since — and that
|
|
241
|
+
* agent, holding no key for it, runs it again.
|
|
242
|
+
*/
|
|
243
|
+
holdsMessage?(sessionId: string, idempotencyKey: string): Promise<boolean>;
|
|
244
|
+
/**
|
|
245
|
+
* A message that reaches a session without running a turn: something
|
|
246
|
+
* said in a conversation the agent is in, to somebody else. The next
|
|
247
|
+
* turn the agent takes has it in hand; nothing is posted now.
|
|
248
|
+
*
|
|
249
|
+
* Resolves `undefined` when there is no such session: an agent hears
|
|
250
|
+
* only conversations it is already in, and the host is the one to say
|
|
251
|
+
* so — on the session's own chain, behind any turn queued ahead, which
|
|
252
|
+
* is what an adapter cannot do from outside. An adapter that checked
|
|
253
|
+
* membership itself first would find nothing for an agent whose
|
|
254
|
+
* invitation is still being written, and drop the message for good.
|
|
255
|
+
*
|
|
256
|
+
* Optional, like `sessionRouting`. A host that omits it gives up
|
|
257
|
+
* overhearing: its agents hear only what they answer, which is what
|
|
258
|
+
* every agent did before this existed.
|
|
259
|
+
*/
|
|
260
|
+
observe?(input: {
|
|
261
|
+
sessionId: string;
|
|
262
|
+
agentId?: string;
|
|
263
|
+
message: string;
|
|
264
|
+
metadata?: JsonObject;
|
|
265
|
+
}): Promise<Session | undefined>;
|
|
266
|
+
/**
|
|
267
|
+
* Settles a call parked on `tool.approval-requested`. False means the
|
|
268
|
+
* request is no longer pending — decided already, expired, or its turn
|
|
269
|
+
* cancelled — which an adapter reports back to whoever answered rather
|
|
270
|
+
* than retrying.
|
|
271
|
+
*
|
|
272
|
+
* Who *may* answer is the adapter's question, not the gateway's: the
|
|
273
|
+
* approver set is written in the channel's own user ids, and posting a
|
|
274
|
+
* request into a conversation must never make everyone in it an approver.
|
|
275
|
+
*/
|
|
276
|
+
resolveApproval(input: {
|
|
277
|
+
requestId: string;
|
|
278
|
+
answer: ApprovalAnswer;
|
|
279
|
+
actor?: string;
|
|
280
|
+
/**
|
|
281
|
+
* `undeliverable` when the adapter settled the request itself because
|
|
282
|
+
* it could not put it to anyone. Defaults to `decided`, which means a
|
|
283
|
+
* person answered — never claim it for an automatic denial.
|
|
284
|
+
*/
|
|
285
|
+
reason?: 'decided' | 'undeliverable';
|
|
286
|
+
}): boolean;
|
|
287
|
+
/**
|
|
288
|
+
* Answers a `credential.requested` with the value a person entered.
|
|
289
|
+
* `ok: false` carries a sentence to show them. `retired` on it means no
|
|
290
|
+
* answer to this request can ever land (the name was stored since, or
|
|
291
|
+
* the request is gone), so the adapter should take the form down rather
|
|
292
|
+
* than leave a button that can only be refused.
|
|
293
|
+
*
|
|
294
|
+
* As with approvals, who may answer is the adapter's question: the
|
|
295
|
+
* approver set is written in the channel's own user ids. The value is
|
|
296
|
+
* handed over here and nowhere else — an adapter never logs it, posts it,
|
|
297
|
+
* or passes it to `dispatch`.
|
|
298
|
+
*
|
|
299
|
+
* Optional: a host that omits it gives up taking credentials from a
|
|
300
|
+
* channel, and an adapter says so to whoever tried instead of offering a
|
|
301
|
+
* form that cannot land.
|
|
302
|
+
*/
|
|
303
|
+
provideCredential?(input: {
|
|
304
|
+
requestId: string;
|
|
305
|
+
value: string;
|
|
306
|
+
actor?: string;
|
|
307
|
+
}): Promise<{
|
|
308
|
+
ok: true;
|
|
309
|
+
} | {
|
|
310
|
+
ok: false;
|
|
311
|
+
message: string;
|
|
312
|
+
retired?: boolean;
|
|
313
|
+
}>;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* A destination this adapter is being asked to speak to, outside any
|
|
317
|
+
* conversation it is currently rendering.
|
|
318
|
+
*
|
|
319
|
+
* The agent is part of the address, not ambient context: each agent is its
|
|
320
|
+
* own app on the platform — its own tokens, possibly its own workspace — so
|
|
321
|
+
* a destination id alone does not say whose credentials it should be
|
|
322
|
+
* resolved against, or even whose id space it belongs to.
|
|
323
|
+
*/
|
|
324
|
+
export interface OutboundAddress {
|
|
325
|
+
/** Whose app must do the talking. */
|
|
326
|
+
agentId: string;
|
|
327
|
+
/**
|
|
328
|
+
* Channel-native destination id — for Slack a channel or DM id
|
|
329
|
+
* (`C…`/`G…`/`D…`), the same convention approver lists already use.
|
|
330
|
+
* Never a Stratus identity: mapping through one adds a lookup that can
|
|
331
|
+
* only be wrong.
|
|
332
|
+
*/
|
|
333
|
+
to: string;
|
|
334
|
+
/**
|
|
335
|
+
* Channel-native id of a message in that conversation to reply under
|
|
336
|
+
* (for Slack, its `ts`). Absent, a post goes to the top level. Only what
|
|
337
|
+
* `post` sends is threaded: it names where the words go, not a second
|
|
338
|
+
* destination, so it is not part of what an approval or a schedule's
|
|
339
|
+
* pre-authorization compares.
|
|
340
|
+
*/
|
|
341
|
+
thread?: string;
|
|
342
|
+
/**
|
|
343
|
+
* Who is speaking: the session whose turn asked for the post, and the
|
|
344
|
+
* label its words carry (`sessionWriteTrust` of that session, so never
|
|
345
|
+
* above `agent`). An adapter uses it to let the posting agent hear its
|
|
346
|
+
* own threaded post in that thread's session, so a reply under a report
|
|
347
|
+
* has the report in context. Absent, nobody vouches for the words and
|
|
348
|
+
* an adapter observes nothing.
|
|
349
|
+
*/
|
|
350
|
+
speaker?: {
|
|
351
|
+
sessionId: string;
|
|
352
|
+
trust: TrustLevel;
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* A request to read a conversation's recent messages, outside any
|
|
357
|
+
* conversation the adapter is rendering — the read twin of
|
|
358
|
+
* `OutboundAddress`. Every id is channel-native (for Slack, a channel id
|
|
359
|
+
* and message `ts` values), for the same reason `OutboundAddress.to` is.
|
|
360
|
+
*/
|
|
361
|
+
export interface ConversationReadRequest {
|
|
362
|
+
/** Whose app does the reading: its membership is the boundary. */
|
|
363
|
+
agentId: string;
|
|
364
|
+
/** Channel-native conversation id, e.g. `C0123456`. */
|
|
365
|
+
conversation: string;
|
|
366
|
+
/**
|
|
367
|
+
* The root message id of one thread. Given, the read is that thread's
|
|
368
|
+
* root and replies, oldest first; absent, the conversation's top level,
|
|
369
|
+
* newest first.
|
|
370
|
+
*/
|
|
371
|
+
thread?: string;
|
|
372
|
+
/** Only messages after this message id or time, exclusive. */
|
|
373
|
+
after?: string;
|
|
374
|
+
/** Only messages before this message id or time, exclusive. */
|
|
375
|
+
before?: string;
|
|
376
|
+
/** At most this many messages; the caller has already bounded it. */
|
|
377
|
+
limit: number;
|
|
378
|
+
/**
|
|
379
|
+
* The session asking, which the gateway always names. A direct message
|
|
380
|
+
* is readable only from a session living in it, which the adapter
|
|
381
|
+
* decides with its own `sessionConversation`; absent, no DM is.
|
|
382
|
+
*/
|
|
383
|
+
sessionId?: string;
|
|
384
|
+
}
|
|
385
|
+
/** One message as a read returns it: text and who wrote it, not markup. */
|
|
386
|
+
export interface ConversationMessage {
|
|
387
|
+
/** Channel-native message id (Slack: its `ts`). */
|
|
388
|
+
id: string;
|
|
389
|
+
/** Channel-native author id; a bot's id when a bot wrote it. */
|
|
390
|
+
author: string;
|
|
391
|
+
/** A display name, when the platform supplies one cheaply. */
|
|
392
|
+
authorName?: string;
|
|
393
|
+
text: string;
|
|
394
|
+
/** ISO-8601 time the message was posted, when derivable. */
|
|
395
|
+
at?: string;
|
|
396
|
+
/** The root message id, for a reply inside a thread. */
|
|
397
|
+
thread?: string;
|
|
398
|
+
/** How many replies a thread root has. */
|
|
399
|
+
replies?: number;
|
|
400
|
+
/** Names of attached files; their contents are not read. */
|
|
401
|
+
files?: string[];
|
|
402
|
+
}
|
|
403
|
+
/** One of a conversation's bookmarks: plain text, no markup and no ids. */
|
|
404
|
+
export interface ConversationBookmark {
|
|
405
|
+
title: string;
|
|
406
|
+
link: string;
|
|
407
|
+
}
|
|
408
|
+
export interface ConversationReadResult {
|
|
409
|
+
messages: ConversationMessage[];
|
|
410
|
+
/** True when the platform had more messages past `limit`. */
|
|
411
|
+
more: boolean;
|
|
412
|
+
/**
|
|
413
|
+
* The conversation's bookmarks, in the platform's order, on a read of its
|
|
414
|
+
* top level with no `before` or `after`, when it has any. Absent on
|
|
415
|
+
* threads and later pages, and on a platform without bookmarks. Written
|
|
416
|
+
* by the people in the conversation, like its messages.
|
|
417
|
+
*/
|
|
418
|
+
bookmarks?: ConversationBookmark[];
|
|
419
|
+
/**
|
|
420
|
+
* Why the bookmarks could not be listed, as a sentence for the agent,
|
|
421
|
+
* when a read that would list them could not. The messages still come
|
|
422
|
+
* back.
|
|
423
|
+
*/
|
|
424
|
+
bookmarksUnavailable?: string;
|
|
425
|
+
/** How many bookmarks did not fit in the read's budget and were left out. */
|
|
426
|
+
bookmarksOmitted?: number;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* A request to create a conversation through the agent's own app — what
|
|
430
|
+
* `channel.create` resolves. Every id is channel-native, like
|
|
431
|
+
* `ConversationReadRequest`'s.
|
|
432
|
+
*/
|
|
433
|
+
export interface ConversationCreateRequest {
|
|
434
|
+
/** Whose app creates it: that app becomes its first member. */
|
|
435
|
+
agentId: string;
|
|
436
|
+
/** The new conversation's name, without a leading `#`. */
|
|
437
|
+
name: string;
|
|
438
|
+
/** Private rather than public. */
|
|
439
|
+
private?: boolean;
|
|
440
|
+
topic?: string;
|
|
441
|
+
purpose?: string;
|
|
442
|
+
/**
|
|
443
|
+
* Who to invite: channel-native user ids, or ids of agents on this host,
|
|
444
|
+
* which the adapter resolves to the user its app for that agent posts as.
|
|
445
|
+
*/
|
|
446
|
+
invite?: string[];
|
|
447
|
+
}
|
|
448
|
+
export interface ConversationCreateResult {
|
|
449
|
+
/** Channel-native id of the new conversation (Slack: `C…`). */
|
|
450
|
+
conversation: string;
|
|
451
|
+
/** Its name as the platform stored it. */
|
|
452
|
+
name: string;
|
|
453
|
+
/** Each `invite` entry that was invited, as the caller named it. */
|
|
454
|
+
invited: string[];
|
|
455
|
+
/** Each `invite` entry that was not, with a sentence for the agent. */
|
|
456
|
+
notInvited: Array<{
|
|
457
|
+
who: string;
|
|
458
|
+
reason: string;
|
|
459
|
+
}>;
|
|
460
|
+
/** A topic or purpose that was asked for and not set, and why. */
|
|
461
|
+
notSet: Array<{
|
|
462
|
+
field: 'topic' | 'purpose';
|
|
463
|
+
reason: string;
|
|
464
|
+
}>;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* A credential an agent asked for, handed to the channel its conversation
|
|
468
|
+
* is in so a person there can be shown a form. Everything the answer needs
|
|
469
|
+
* stays with the gateway under `requestId`; the channel only has to show
|
|
470
|
+
* the question and quote the id back through `GatewayLike.provideCredential`.
|
|
471
|
+
*/
|
|
472
|
+
export interface ChannelCredentialRequest {
|
|
473
|
+
sessionId: string;
|
|
474
|
+
agentId: string;
|
|
475
|
+
requestId: string;
|
|
476
|
+
name: string;
|
|
477
|
+
scope: CredentialScope;
|
|
478
|
+
/** The agent's own words for why; show them as the agent's. */
|
|
479
|
+
reason?: string;
|
|
480
|
+
/**
|
|
481
|
+
* A new value for a key the agent already holds, swapped in place. Only
|
|
482
|
+
* an approver may answer it, as with an add; the form should say a key
|
|
483
|
+
* is being replaced.
|
|
484
|
+
*/
|
|
485
|
+
replace?: boolean;
|
|
486
|
+
/** The session's routing metadata: where the conversation is. */
|
|
487
|
+
metadata: JsonObject;
|
|
488
|
+
}
|
|
489
|
+
export interface ChannelAdapter {
|
|
490
|
+
name: string;
|
|
491
|
+
start(gateway: GatewayLike): Promise<void>;
|
|
492
|
+
stop(): Promise<void>;
|
|
493
|
+
/**
|
|
494
|
+
* The write side of an addressable destination — what `message.send` and
|
|
495
|
+
* schedule-creation validation resolve. Optional because it is a
|
|
496
|
+
* capability, not a courtesy: a transport with no concept of a
|
|
497
|
+
* destination simply does not have the method, and a caller that needs
|
|
498
|
+
* it checks, the same way `GatewayLike.sessionRouting` works.
|
|
499
|
+
*
|
|
500
|
+
* The contract is validate-then-hand-over: an implementation MUST refuse
|
|
501
|
+
* — by rejecting, with a message fit to show the person who named the
|
|
502
|
+
* destination — anything it could not actually deliver to (an agent with
|
|
503
|
+
* no app here, a conversation that does not exist or that the agent's
|
|
504
|
+
* app is not a member of), rather than returning a connection whose
|
|
505
|
+
* first `post` fails at 6am with nobody watching. Resolving is therefore
|
|
506
|
+
* also how a destination is checked without sending anything.
|
|
507
|
+
*/
|
|
508
|
+
resolveOutbound?(address: OutboundAddress): Promise<OutboundConnection>;
|
|
509
|
+
/**
|
|
510
|
+
* Shows a person in the conversation a form for a credential the agent
|
|
511
|
+
* asked for. Optional, like `resolveOutbound`: a channel without it
|
|
512
|
+
* cannot take credentials, and the gateway refuses a request made in its
|
|
513
|
+
* conversations rather than telling the agent someone was asked.
|
|
514
|
+
*
|
|
515
|
+
* Resolves once the form is in front of someone who can answer it, and
|
|
516
|
+
* MUST reject otherwise (the post failed, nobody here may answer, the
|
|
517
|
+
* agent has no app connected), with a sentence for the agent: the
|
|
518
|
+
* gateway drops the request and hands the agent that sentence, because
|
|
519
|
+
* a request nobody can see would otherwise wait forever while the agent
|
|
520
|
+
* tells its user the operator was asked.
|
|
521
|
+
*/
|
|
522
|
+
requestCredential?(request: ChannelCredentialRequest): Promise<void>;
|
|
523
|
+
/**
|
|
524
|
+
* True when this adapter's `requestCredential` form honors `replace`:
|
|
525
|
+
* it tells the approver the stored key will be overwritten. Absent on an
|
|
526
|
+
* adapter written before replacements, which the gateway never hands one,
|
|
527
|
+
* since its ordinary add form would rotate a key without saying so.
|
|
528
|
+
*/
|
|
529
|
+
credentialReplaceForms?: boolean;
|
|
530
|
+
/**
|
|
531
|
+
* The read side of an addressable conversation — what `message.read`
|
|
532
|
+
* resolves. Optional, like `resolveOutbound`: a transport with no
|
|
533
|
+
* history (or none it will share) lacks the method, and the gateway
|
|
534
|
+
* says the channel cannot be read.
|
|
535
|
+
*
|
|
536
|
+
* MUST reject, with a sentence for the agent, anything its app could
|
|
537
|
+
* not or should not read: an agent with no app here, a conversation it
|
|
538
|
+
* is not a member of, and any kind of conversation the adapter keeps
|
|
539
|
+
* private to the people in it. The result is quoted to the model, so
|
|
540
|
+
* an implementation returns text, never platform markup or tokens.
|
|
541
|
+
* The one exception to private conversations is the one the asking
|
|
542
|
+
* session lives in (`request.sessionId`, through `sessionConversation`).
|
|
543
|
+
*/
|
|
544
|
+
readConversation?(request: ConversationReadRequest): Promise<ConversationReadResult>;
|
|
545
|
+
/**
|
|
546
|
+
* The conversation a session lives in, as this agent's own app here
|
|
547
|
+
* sees it: the conversation id its session key names, when the session
|
|
548
|
+
* id is exactly that key (a thread inside it included). Undefined for
|
|
549
|
+
* any other session: another agent's, another workspace's, a delegated
|
|
550
|
+
* or scheduled one, an archived one, or one on another transport, and
|
|
551
|
+
* for an agent with no connected app here. Decided from the id alone,
|
|
552
|
+
* with no lookup. Both `message.read`'s refusal and the adapter's own
|
|
553
|
+
* `readConversation` ask it, so the two layers cannot disagree about
|
|
554
|
+
* which direct message a session may read: only its own. Optional:
|
|
555
|
+
* without it, no direct message is readable.
|
|
556
|
+
*/
|
|
557
|
+
sessionConversation?(agentId: string, sessionId: string): string | undefined;
|
|
558
|
+
/**
|
|
559
|
+
* Creates a conversation with the agent's own app and invites people to
|
|
560
|
+
* it — what `channel.create` resolves. Optional, like `readConversation`:
|
|
561
|
+
* a transport that cannot create conversations lacks the method, and the
|
|
562
|
+
* gateway says so.
|
|
563
|
+
*
|
|
564
|
+
* MUST reject, with a sentence for the agent, a request it can tell is
|
|
565
|
+
* invalid before creating anything (a name, topic or purpose the
|
|
566
|
+
* platform would refuse), so a bad request leaves nothing behind, and a
|
|
567
|
+
* create the platform refuses (no app here, a missing permission, a
|
|
568
|
+
* taken name). Once the conversation exists it MUST resolve: an invite,
|
|
569
|
+
* topic or purpose that then fails goes in `notInvited` or `notSet`,
|
|
570
|
+
* never undoes the conversation, and never turns into a rejection the
|
|
571
|
+
* agent would retry into a second one.
|
|
572
|
+
*/
|
|
573
|
+
createConversation?(request: ConversationCreateRequest): Promise<ConversationCreateResult>;
|
|
574
|
+
/**
|
|
575
|
+
* The workspace this agent's app is connected to, for a transport whose
|
|
576
|
+
* conversation ids are unique only within one: a Slack team id.
|
|
577
|
+
* Undefined when the agent has no app here or it has not authenticated.
|
|
578
|
+
* A standing per-conversation grant records it and matches only under
|
|
579
|
+
* the same one. Optional: a transport whose ids are globally unique
|
|
580
|
+
* lacks the method.
|
|
581
|
+
*/
|
|
582
|
+
accountFor?(agentId: string): string | undefined;
|
|
583
|
+
/**
|
|
584
|
+
* Whether a conversation is a channel (`channel`, public or private) or
|
|
585
|
+
* a direct message (`direct`, including a group DM), as this agent's own
|
|
586
|
+
* app sees it. Undefined when it cannot say: no app, not connected, or
|
|
587
|
+
* a conversation it cannot see. The operator's standing channel
|
|
588
|
+
* authorization (`approvals.channels`) covers `channel` only, so an
|
|
589
|
+
* implementation never guesses `channel` from an id's shape. Optional:
|
|
590
|
+
* without it, that authorization never applies on this transport.
|
|
591
|
+
*/
|
|
592
|
+
conversationKindFor?(agentId: string, conversation: string): Promise<'channel' | 'direct' | undefined>;
|
|
593
|
+
}
|
|
594
|
+
export interface ChannelSessionKeyParts {
|
|
595
|
+
/** Channel kind, e.g. 'slack'. */
|
|
596
|
+
channel: string;
|
|
597
|
+
/** The addressed agent — part of the key, so sessions never cross identities. */
|
|
598
|
+
agentId: string;
|
|
599
|
+
team: string;
|
|
600
|
+
conversation: string;
|
|
601
|
+
/**
|
|
602
|
+
* Thread id for threaded containers. Omit for a DM's top level, which is
|
|
603
|
+
* one ongoing conversation per peer keyed by the conversation alone; a
|
|
604
|
+
* thread inside a DM passes its root and is a session of its own.
|
|
605
|
+
*/
|
|
606
|
+
thread?: string;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* The stable session id for one conversation: `channel:agent:team:conversation[:thread]`.
|
|
610
|
+
* Stable means resumable — any later message with the same parts lands in
|
|
611
|
+
* the same session, across daemon restarts included. Callers pass
|
|
612
|
+
* `thread_ts ?? ts` as the thread for channel messages (a top-level mention
|
|
613
|
+
* roots its own reply-thread conversation), no thread for a DM's top level,
|
|
614
|
+
* and the thread's root for a reply in a DM thread.
|
|
615
|
+
*/
|
|
616
|
+
export declare const channelSessionKey: (parts: ChannelSessionKeyParts) => string;
|
|
617
|
+
/**
|
|
618
|
+
* Whether a sender outside the principals list gets a turn at all.
|
|
619
|
+
* `anyone` admits them, labelled `unknown`; `principals` refuses them
|
|
620
|
+
* before a turn starts, and an adapter that overhears must not let the
|
|
621
|
+
* agent overhear them either — the text would be in the transcript, which
|
|
622
|
+
* is what this mode exists to keep out.
|
|
623
|
+
*/
|
|
624
|
+
export type AdmitPolicy = 'anyone' | 'principals';
|
|
625
|
+
/**
|
|
626
|
+
* Who a channel treats as the operator, and whether anyone else is let in.
|
|
627
|
+
* Ids are the channel's own (a Slack user id, a phone number) — never a
|
|
628
|
+
* Stratus identity, for the same reason `OutboundAddress.to` is not.
|
|
629
|
+
*
|
|
630
|
+
* The two fields answer different questions. `principals` is provenance:
|
|
631
|
+
* an adapter's own admission checks (a DM, a mention) establish nothing
|
|
632
|
+
* about who is typing, and this list is what makes the `user` label mean
|
|
633
|
+
* something. `admit` is authorization. Absent `principals` and `[]` both
|
|
634
|
+
* mean nobody is a principal; an adapter may still tell them apart for
|
|
635
|
+
* presentation (Slack shows everyone's name when there is no list).
|
|
636
|
+
*
|
|
637
|
+
* Which `admit` applies when config gives none is the adapter's call, not
|
|
638
|
+
* this module's, and the daemon's config resolves an absent one to
|
|
639
|
+
* `principals` before any adapter sees it. Pass the policy with that
|
|
640
|
+
* default already applied.
|
|
641
|
+
*/
|
|
642
|
+
export interface SenderPolicy {
|
|
643
|
+
principals?: readonly string[];
|
|
644
|
+
admit?: AdmitPolicy;
|
|
645
|
+
}
|
|
646
|
+
/** Whether `senderId` is on the policy's principals list. */
|
|
647
|
+
export declare const isPrincipal: (policy: SenderPolicy | undefined, senderId: string) => boolean;
|
|
648
|
+
/**
|
|
649
|
+
* Whether a message from `senderId` may start a turn. No policy admits:
|
|
650
|
+
* there is nobody's list to refuse against, which is the answer from
|
|
651
|
+
* before admission existed.
|
|
652
|
+
*/
|
|
653
|
+
export declare const admitsSender: (policy: SenderPolicy | undefined, senderId: string) => boolean;
|
|
654
|
+
/**
|
|
655
|
+
* The trust label a turn from `senderId` carries (`SENDER_TRUST_METADATA_KEY`
|
|
656
|
+
* in `@stratusagent/core`): `user` for a principal, `unknown` for anyone else.
|
|
657
|
+
* Evaluated per message, never per session: a group conversation keys one
|
|
658
|
+
* session for everyone in it.
|
|
659
|
+
*/
|
|
660
|
+
export declare const senderTrustFor: (policy: SenderPolicy | undefined, senderId: string) => Extract<TrustLevel, "user" | "unknown">;
|
|
661
|
+
//# sourceMappingURL=channels.d.ts.map
|