@stratusagent/core 0.13.5 → 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 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