@stratusagent/gateway 0.14.7 → 0.14.8
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/dist/index.d.ts +4 -1290
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -279
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.d.ts +130 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +147 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/session-keys.d.ts +101 -0
- package/dist/session-keys.d.ts.map +1 -0
- package/dist/session-keys.js +148 -0
- package/dist/session-keys.js.map +1 -0
- package/dist/types.d.ts +1103 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +7 -7
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,1103 @@
|
|
|
1
|
+
import type { AgentDefinition, AlwaysMeans, ApprovalAnswer, ApprovalDelegationOrigin, ApprovalOutcome, ApprovalPolicy, ApprovalScheduleDestination, CredentialScope, EventBus, ImageAttachment, JsonObject, ObserveEntry, Session, Tool, ToolCall, ToolRisk, TrustLevel, UnlistedTools } from '@stratusagent/core';
|
|
2
|
+
import type { ConversationArchiver, ConversationCreator, ConversationInviter, ConversationJoiner, ConversationReader, ParsedSoul, ScheduleRecord } from '@stratusagent/agents';
|
|
3
|
+
import type { OptionalModuleHost } from '@stratusagent/plugins';
|
|
4
|
+
import type { MemoryPassSummary, PluginsConfig, RuntimeSelection, StateEnvironment } from '@stratusagent/state';
|
|
5
|
+
import type { ChannelReloadOutcome, EventDeliveryRow, EventIntakeRequest, EventIntakeResponse, EventSourceConfig, EventsSummary, IntakeState, MemoryPassDetail, RestartOutcome, RestartRequest, RestartStatus, SchedulerLimits, ShardedSessionStore } from './index.ts';
|
|
6
|
+
/**
|
|
7
|
+
* A channel adapter as the gateway sees it: started after the roster
|
|
8
|
+
* loads, stopped before the store drains. Structurally identical to
|
|
9
|
+
* `ChannelAdapter` in @stratusagent/core, declared here because it
|
|
10
|
+
* predates the channel contract moving into core, when the gateway could
|
|
11
|
+
* not import it without depending on the channels package.
|
|
12
|
+
*/
|
|
13
|
+
export interface GatewayChannelAdapter {
|
|
14
|
+
name: string;
|
|
15
|
+
start(gateway: Gateway): Promise<void>;
|
|
16
|
+
stop(): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Whether the gateway may serve without this channel.
|
|
19
|
+
*
|
|
20
|
+
* A channel whose `start()` rejects is logged and skipped, which is right
|
|
21
|
+
* for a Slack app whose tokens went stale: the rest of the fleet stays
|
|
22
|
+
* up. A required channel fails `start()` instead — after stopping the
|
|
23
|
+
* channels that did come up, and before the scheduler starts or any
|
|
24
|
+
* sweep re-asks an approval, so a daemon refused this way has done no
|
|
25
|
+
* work. The control API is the case: a second `stratus serve` on the
|
|
26
|
+
* same home lost the port to the first, logged it, and went on serving
|
|
27
|
+
* with no channel at all — and the next scheduled firing ran in it,
|
|
28
|
+
* where nobody could approve or hear anything. A host that omits this
|
|
29
|
+
* gets the skip.
|
|
30
|
+
*/
|
|
31
|
+
required?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Whether this channel starts while the gateway is held (`held: true`).
|
|
34
|
+
* Only a surface for the operator should: the control API, so health can
|
|
35
|
+
* be asked and `promote` sent. A conversation channel must not, or the
|
|
36
|
+
* held daemon would take work. Absent means it waits for `promote()`.
|
|
37
|
+
*/
|
|
38
|
+
servesWhileHeld?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* The addressable outbound seam, mirroring `ChannelAdapter.resolveOutbound`
|
|
41
|
+
* structurally for the same reason the rest of this interface does. Optional: an
|
|
42
|
+
* adapter with no concept of a destination simply lacks it, and the
|
|
43
|
+
* gateway treats absence as "this channel cannot be spoken through".
|
|
44
|
+
* Implementations MUST reject a destination they could not deliver to,
|
|
45
|
+
* with a message fit to show the person who named it.
|
|
46
|
+
*/
|
|
47
|
+
resolveOutbound?(address: {
|
|
48
|
+
agentId: string;
|
|
49
|
+
to: string;
|
|
50
|
+
thread?: string;
|
|
51
|
+
speaker?: {
|
|
52
|
+
sessionId: string;
|
|
53
|
+
trust: TrustLevel;
|
|
54
|
+
};
|
|
55
|
+
}): Promise<{
|
|
56
|
+
post(text: string): Promise<unknown>;
|
|
57
|
+
}>;
|
|
58
|
+
/**
|
|
59
|
+
* The read side of an addressable conversation, mirroring
|
|
60
|
+
* `ChannelAdapter.readConversation`. Optional:
|
|
61
|
+
* an adapter without it cannot be read through `message.read`.
|
|
62
|
+
* Implementations MUST reject a conversation their app may not read,
|
|
63
|
+
* with a sentence for the agent.
|
|
64
|
+
*/
|
|
65
|
+
readConversation?(request: {
|
|
66
|
+
agentId: string;
|
|
67
|
+
conversation: string;
|
|
68
|
+
thread?: string;
|
|
69
|
+
after?: string;
|
|
70
|
+
before?: string;
|
|
71
|
+
limit: number;
|
|
72
|
+
sessionId?: string;
|
|
73
|
+
images?: {
|
|
74
|
+
count: number;
|
|
75
|
+
bytes: number;
|
|
76
|
+
};
|
|
77
|
+
}): ReturnType<ConversationReader>;
|
|
78
|
+
/**
|
|
79
|
+
* Creates a conversation with the agent's own app, mirroring
|
|
80
|
+
* `ChannelAdapter.createConversation`. Optional: an adapter without it
|
|
81
|
+
* cannot serve `channel.create`. Implementations MUST reject what they
|
|
82
|
+
* can tell is invalid before creating anything, and MUST resolve once
|
|
83
|
+
* the conversation exists, reporting what did not happen after it.
|
|
84
|
+
*/
|
|
85
|
+
createConversation?(request: Omit<Parameters<ConversationCreator>[0], 'channel'>): ReturnType<ConversationCreator>;
|
|
86
|
+
/**
|
|
87
|
+
* Joins an existing conversation with the agent's own app, mirroring
|
|
88
|
+
* `ChannelAdapter.joinConversation`. Optional: an adapter without it
|
|
89
|
+
* cannot serve `channel.join`.
|
|
90
|
+
*/
|
|
91
|
+
joinConversation?(request: Omit<Parameters<ConversationJoiner>[0], 'channel'>): ReturnType<ConversationJoiner>;
|
|
92
|
+
/**
|
|
93
|
+
* Invites people to an existing conversation through the agent's own
|
|
94
|
+
* app, mirroring `ChannelAdapter.inviteToConversation`. Optional: an
|
|
95
|
+
* adapter without it cannot serve `channel.invite`. Implementations
|
|
96
|
+
* MUST reject only when nobody was invited.
|
|
97
|
+
*/
|
|
98
|
+
inviteToConversation?(request: Omit<Parameters<ConversationInviter>[0], 'channel'>): ReturnType<ConversationInviter>;
|
|
99
|
+
/**
|
|
100
|
+
* Archives a conversation the agent's own app created, mirroring
|
|
101
|
+
* `ChannelAdapter.archiveConversation`. Optional: an adapter without it
|
|
102
|
+
* cannot serve `channel.archive`. The gateway has already refused the
|
|
103
|
+
* conversation the work was asked from, which it asks the same adapter's
|
|
104
|
+
* `sessionConversation`; without that, every archive is refused.
|
|
105
|
+
*/
|
|
106
|
+
archiveConversation?(request: Omit<Parameters<ConversationArchiver>[0], 'channel'>): ReturnType<ConversationArchiver>;
|
|
107
|
+
/** See `ChannelAdapter.accountFor`. */
|
|
108
|
+
accountFor?(agentId: string): string | undefined;
|
|
109
|
+
/** See `ChannelAdapter.conversationKindFor`. */
|
|
110
|
+
conversationKindFor?(agentId: string, conversation: string): Promise<'channel' | 'direct' | undefined>;
|
|
111
|
+
/** See `ChannelAdapter.sessionConversation`. */
|
|
112
|
+
sessionConversation?(agentId: string, sessionId: string): string | undefined;
|
|
113
|
+
/**
|
|
114
|
+
* Picks up which agents this channel carries without a restart, mirroring
|
|
115
|
+
* `ChannelAdapter.reload`. The gateway calls it on every started channel
|
|
116
|
+
* after a roster reload, one channel at a time, and stops waiting after
|
|
117
|
+
* `channelReloadTimeoutMs`. Optional: a channel without it changes only
|
|
118
|
+
* at a restart.
|
|
119
|
+
*/
|
|
120
|
+
reload?(): Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* Shows a person a form for a credential an agent asked for, mirroring
|
|
123
|
+
* `ChannelAdapter.requestCredential`. Optional:
|
|
124
|
+
* a channel without it cannot take credentials, and a request made in
|
|
125
|
+
* its conversations is refused instead of announced. Implementations
|
|
126
|
+
* MUST reject when the form reached nobody who can answer it, with a
|
|
127
|
+
* sentence for the agent.
|
|
128
|
+
*/
|
|
129
|
+
requestCredential?(request: {
|
|
130
|
+
sessionId: string;
|
|
131
|
+
agentId: string;
|
|
132
|
+
requestId: string;
|
|
133
|
+
name: string;
|
|
134
|
+
scope: CredentialScope;
|
|
135
|
+
reason?: string;
|
|
136
|
+
replace?: boolean;
|
|
137
|
+
env?: string;
|
|
138
|
+
metadata: JsonObject;
|
|
139
|
+
}): Promise<void>;
|
|
140
|
+
/** See `ChannelAdapter.credentialReplaceForms`. */
|
|
141
|
+
credentialReplaceForms?: boolean;
|
|
142
|
+
/** See `ChannelAdapter.credentialEnvForms`. */
|
|
143
|
+
credentialEnvForms?: boolean;
|
|
144
|
+
/** See `ChannelAdapter.say`. */
|
|
145
|
+
say?(request: {
|
|
146
|
+
sessionId: string;
|
|
147
|
+
agentId: string;
|
|
148
|
+
text: string;
|
|
149
|
+
}): Promise<void>;
|
|
150
|
+
/**
|
|
151
|
+
* Issues a one-time link to a form for a credential request, for the
|
|
152
|
+
* agent to pass on: what a request falls back to when its conversation
|
|
153
|
+
* cannot show a form, and what an agent asking `via: 'link'` gets. The
|
|
154
|
+
* control API implements it. Optional: a host with no adapter carrying it
|
|
155
|
+
* refuses such requests with the machine-side command instead.
|
|
156
|
+
*
|
|
157
|
+
* The link is the whole credential: whoever opens it can answer the
|
|
158
|
+
* request once, through `provideCredential`, which stays add-only and
|
|
159
|
+
* grants only the agent that asked.
|
|
160
|
+
*/
|
|
161
|
+
requestCredentialLink?(request: {
|
|
162
|
+
sessionId: string;
|
|
163
|
+
agentId: string;
|
|
164
|
+
agentName: string;
|
|
165
|
+
requestId: string;
|
|
166
|
+
name: string;
|
|
167
|
+
scope: CredentialScope;
|
|
168
|
+
reason?: string;
|
|
169
|
+
/** The variable the agent's shell will read the key from; the page must say so. */
|
|
170
|
+
env?: string;
|
|
171
|
+
}): Promise<{
|
|
172
|
+
url: string;
|
|
173
|
+
expiresAt: string;
|
|
174
|
+
/** The link is on an address only the machine itself can open (no `api.publicUrl`). */
|
|
175
|
+
localOnly?: boolean;
|
|
176
|
+
}>;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Where a session came from, as the gateway reports it. Structural for the
|
|
180
|
+
* same reason as the adapter above: core's channel contract declares the
|
|
181
|
+
* matching shape (`SessionRouting`) for adapters to consume.
|
|
182
|
+
*/
|
|
183
|
+
export interface SessionRouting {
|
|
184
|
+
/** Whose app must do the talking. */
|
|
185
|
+
agentId: string;
|
|
186
|
+
/** The metadata the dispatching surface attached to the session. */
|
|
187
|
+
metadata: JsonObject;
|
|
188
|
+
/**
|
|
189
|
+
* When this agent last spoke, ISO-8601 — its newest reply's timestamp,
|
|
190
|
+
* absent when it has never produced one. Not the session's modification
|
|
191
|
+
* time, which a mid-turn save moves; see `SessionRouting` in @stratusagent/core.
|
|
192
|
+
*/
|
|
193
|
+
lastSpokeAt?: string;
|
|
194
|
+
/** The latest turn's text (`latestTurnReply`), when it produced any — see `SessionRouting` in @stratusagent/core. */
|
|
195
|
+
reply?: string;
|
|
196
|
+
/** Whether the turn the session is on is one nobody asked for (`isUnaddressedTurn`) — see `SessionRouting` in @stratusagent/core. */
|
|
197
|
+
unaddressed?: boolean;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* What the gateway hands a policy that needs a human somewhere else: one
|
|
201
|
+
* function that publishes a request and settles on the answer. Handed to a
|
|
202
|
+
* factory rather than exposed on the built gateway because the policy has
|
|
203
|
+
* to exist before the first dispatch, and a gateway cannot be constructed
|
|
204
|
+
* with a policy that needs the gateway.
|
|
205
|
+
*/
|
|
206
|
+
export interface ApprovalTransport {
|
|
207
|
+
request: ApprovalRequester;
|
|
208
|
+
/**
|
|
209
|
+
* Which plugin package contributes a tool right now, or undefined for a
|
|
210
|
+
* kernel tool (`@stratusagent/permissions`' `grants.contributorOf` takes
|
|
211
|
+
* exactly this shape). Read live from the plugins' tool records rather
|
|
212
|
+
* than snapshotted, for the same reason `tools()` is: a bridge registers
|
|
213
|
+
* what its server advertises, and a grant compared against a stale map
|
|
214
|
+
* would follow the wrong tool.
|
|
215
|
+
*/
|
|
216
|
+
contributorOf(toolName: string): string | undefined;
|
|
217
|
+
/**
|
|
218
|
+
* The schedule carve-out, for a policy that understands destinations
|
|
219
|
+
* (`@stratusagent/permissions`' `destinations` option takes exactly this
|
|
220
|
+
* shape). True when the session is a firing of a schedule that still
|
|
221
|
+
* exists, belongs to this agent, and was approved with this destination
|
|
222
|
+
* — the gateway answers from the schedule row, so cancelling revokes.
|
|
223
|
+
*/
|
|
224
|
+
destinations: {
|
|
225
|
+
isPreauthorized(session: Session, destination: string): boolean;
|
|
226
|
+
/**
|
|
227
|
+
* The workspace the agent's channel for this `channel:to` destination
|
|
228
|
+
* is connected to (`ChannelAdapter.accountFor`), so a standing grant
|
|
229
|
+
* on one conversation is bound to the workspace it was made in.
|
|
230
|
+
*/
|
|
231
|
+
accountFor(agentId: string, destination: string): string | undefined;
|
|
232
|
+
/**
|
|
233
|
+
* Whether a `channel:to` destination is a channel or a DM, asked of the
|
|
234
|
+
* agent's own app for that channel (`ChannelAdapter.conversationKindFor`).
|
|
235
|
+
* Undefined when no adapter can say.
|
|
236
|
+
*/
|
|
237
|
+
conversationKindFor(agentId: string, destination: string): Promise<'channel' | 'direct' | undefined>;
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* The review posture's reviewer (`@stratusagent/permissions`' `review`
|
|
241
|
+
* option wraps it): one bounded model call on the agent's own provider
|
|
242
|
+
* and credentials, exactly the model named (the agent's own when none is)
|
|
243
|
+
* with no fallback and no tools, counted by the provider-call guards like
|
|
244
|
+
* any other call. The case it is shown holds the people's own words and
|
|
245
|
+
* the call, never tool output (`review.ts`).
|
|
246
|
+
*
|
|
247
|
+
* Never rejects: a review that couldn't run answers `failed` with a kind
|
|
248
|
+
* a person reads in the log, and the policy asks about the call.
|
|
249
|
+
*/
|
|
250
|
+
review(request: GatewayReviewRequest): Promise<{
|
|
251
|
+
reply: Record<string, unknown>;
|
|
252
|
+
} | {
|
|
253
|
+
failed: string;
|
|
254
|
+
}>;
|
|
255
|
+
/**
|
|
256
|
+
* The values of the stored credentials this agent's soul lists, for the
|
|
257
|
+
* review posture's built-in rule against sending one out. Read from disk
|
|
258
|
+
* per call, like every credential, so a key set a moment ago counts.
|
|
259
|
+
* Never logged; an unreadable store answers nothing.
|
|
260
|
+
*/
|
|
261
|
+
secretsFor(agentId: string): Promise<string[]>;
|
|
262
|
+
/**
|
|
263
|
+
* Work the policy runs off the call path, such as a shadow review
|
|
264
|
+
* (`@stratusagent/permissions`' `review.offPath`), from its start: its
|
|
265
|
+
* credential read as well as its reviewer call. A shutdown waits for it
|
|
266
|
+
* after the turns have drained, so what it reports isn't lost.
|
|
267
|
+
*/
|
|
268
|
+
offPath(task: Promise<unknown>): void;
|
|
269
|
+
}
|
|
270
|
+
/** One call put to the reviewer. Mirrors `@stratusagent/permissions`' `ReviewRequest`. */
|
|
271
|
+
export interface GatewayReviewRequest {
|
|
272
|
+
session: Session;
|
|
273
|
+
call: ToolCall;
|
|
274
|
+
tool: Tool;
|
|
275
|
+
risk: ToolRisk;
|
|
276
|
+
command?: string;
|
|
277
|
+
origin?: string;
|
|
278
|
+
destination?: string;
|
|
279
|
+
/** `approvals.review.model`; undefined for the agent's own model. */
|
|
280
|
+
model?: string;
|
|
281
|
+
signal: AbortSignal;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* A parked call, published for anyone who can reach a person. Mirrors
|
|
285
|
+
* `@stratusagent/permissions`' `ApprovalRequest` structurally rather than
|
|
286
|
+
* importing it: the gateway consumes any `ApprovalPolicy`, and depending on
|
|
287
|
+
* one implementation of the seam to describe the seam would invert that.
|
|
288
|
+
*/
|
|
289
|
+
export interface GatewayApprovalRequest {
|
|
290
|
+
session: Session;
|
|
291
|
+
call: {
|
|
292
|
+
id: string;
|
|
293
|
+
toolName: string;
|
|
294
|
+
input: JsonObject;
|
|
295
|
+
};
|
|
296
|
+
risk: ToolRisk;
|
|
297
|
+
/**
|
|
298
|
+
* The site this call would act on, for a tool judged by one
|
|
299
|
+
* (`Tool.originFor`). Passed straight through to whatever renders the
|
|
300
|
+
* request, because the call's arguments do not say where a click lands.
|
|
301
|
+
*/
|
|
302
|
+
origin?: string;
|
|
303
|
+
/**
|
|
304
|
+
* True when answering `always` runs this call once and remembers
|
|
305
|
+
* nothing, so a renderer can stop offering one. Passed straight through.
|
|
306
|
+
*/
|
|
307
|
+
oneShot?: boolean;
|
|
308
|
+
/** What `always` would remember, when it would. Passed straight through. */
|
|
309
|
+
always?: AlwaysMeans;
|
|
310
|
+
/** Why the review posture is asking. Passed straight through. */
|
|
311
|
+
reviewNote?: string;
|
|
312
|
+
/**
|
|
313
|
+
* When this call first parked, if a restart is re-asking it. The wait is
|
|
314
|
+
* measured from here, so a request keeps the window it started with
|
|
315
|
+
* instead of winning a fresh one — otherwise a daemon that restarts a
|
|
316
|
+
* minute before the deadline grants another full timeout, and one that
|
|
317
|
+
* crash-loops keeps a request alive forever.
|
|
318
|
+
*/
|
|
319
|
+
parkedAt?: string;
|
|
320
|
+
signal?: AbortSignal;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Settles with the answer and, when a person gave it, who — the policy
|
|
324
|
+
* records the actor on a standing grant. Every ending settles: a timeout,
|
|
325
|
+
* an abort, and a shutdown are each a `deny` with nobody behind it.
|
|
326
|
+
*/
|
|
327
|
+
export type ApprovalRequester = (request: GatewayApprovalRequest) => Promise<ApprovalOutcome>;
|
|
328
|
+
/**
|
|
329
|
+
* A parked call as an observer sees it — everything `tool.approval-requested`
|
|
330
|
+
* announced, still outstanding.
|
|
331
|
+
*
|
|
332
|
+
* Listable because an event stream only tells you what happened while you
|
|
333
|
+
* were listening. A surface that connects while a turn is already parked
|
|
334
|
+
* would otherwise show no pending request at all until the next one, and
|
|
335
|
+
* rebuilding the set from the bus means keeping a second copy of state the
|
|
336
|
+
* gateway already holds.
|
|
337
|
+
*/
|
|
338
|
+
export interface PendingApproval {
|
|
339
|
+
requestId: string;
|
|
340
|
+
sessionId: string;
|
|
341
|
+
agentId: string;
|
|
342
|
+
call: {
|
|
343
|
+
id: string;
|
|
344
|
+
toolName: string;
|
|
345
|
+
input: JsonObject;
|
|
346
|
+
};
|
|
347
|
+
risk: ToolRisk;
|
|
348
|
+
/** The site it would act on, for a tool judged by one. */
|
|
349
|
+
origin?: string;
|
|
350
|
+
/** True when `always` on this call remembers nothing — see the event. */
|
|
351
|
+
oneShot?: boolean;
|
|
352
|
+
/** What `always` would remember, when it would — see the event. */
|
|
353
|
+
always?: AlwaysMeans;
|
|
354
|
+
/** Why the review posture is asking — see the event. */
|
|
355
|
+
reviewNote?: string;
|
|
356
|
+
/** When this call parked, ISO-8601. */
|
|
357
|
+
parkedAt: string;
|
|
358
|
+
/**
|
|
359
|
+
* When it denies itself, ISO-8601. Absent when it never will — the same
|
|
360
|
+
* distinction the announcing event draws.
|
|
361
|
+
*/
|
|
362
|
+
expiresAt?: string;
|
|
363
|
+
/** The session's metadata: where the turn is happening. */
|
|
364
|
+
metadata?: JsonObject;
|
|
365
|
+
/** Where a delegated call's work was asked from — see the event. */
|
|
366
|
+
delegatedFrom?: ApprovalDelegationOrigin;
|
|
367
|
+
/** Where a scheduled firing's schedule reports — see the event. */
|
|
368
|
+
scheduleDestination?: ApprovalScheduleDestination;
|
|
369
|
+
}
|
|
370
|
+
export interface ResolveApprovalInput {
|
|
371
|
+
requestId: string;
|
|
372
|
+
answer: ApprovalAnswer;
|
|
373
|
+
/**
|
|
374
|
+
* The person who decided, in whatever ids their channel uses. Recorded on
|
|
375
|
+
* the resolved event; who is *allowed* to decide is the channel's
|
|
376
|
+
* question, since the approver set is written in its ids.
|
|
377
|
+
*/
|
|
378
|
+
actor?: string;
|
|
379
|
+
/**
|
|
380
|
+
* Why, when it was not a person. A channel that cannot deliver a request
|
|
381
|
+
* — nobody configured to ask, nowhere to ask — still has to settle it,
|
|
382
|
+
* and recording that as `decided` would file a denial nobody made
|
|
383
|
+
* alongside the ones somebody did. `timeout` and `cancelled` are not
|
|
384
|
+
* offered here: those are the gateway's own endings to declare.
|
|
385
|
+
*/
|
|
386
|
+
reason?: 'decided' | 'undeliverable';
|
|
387
|
+
}
|
|
388
|
+
/** One registered tool, as an operator's surfaces need to see it. */
|
|
389
|
+
export interface GatewayTool {
|
|
390
|
+
name: string;
|
|
391
|
+
description?: string;
|
|
392
|
+
risk: ToolRisk;
|
|
393
|
+
/** The plugin package that contributed it; absent for kernel tools. */
|
|
394
|
+
package?: string;
|
|
395
|
+
/** Whether that package is trusted to declare a tool `safe`. */
|
|
396
|
+
trusted?: boolean;
|
|
397
|
+
/**
|
|
398
|
+
* What a call to it is judged by, when it offers exactly one scope hook
|
|
399
|
+
* (`Tool.commandFor`, `originFor`, `destinationFor`): what an
|
|
400
|
+
* `approvals.rules` matcher for it can name. Absent for none or several.
|
|
401
|
+
*/
|
|
402
|
+
scopedBy?: 'command' | 'origin' | 'destination';
|
|
403
|
+
}
|
|
404
|
+
/** One skill this daemon serves — identity and provenance, never the body. */
|
|
405
|
+
export interface GatewaySkill {
|
|
406
|
+
/** Canonical id: bare for an operator-installed skill, `<package>:<skill>` for a plugin's. */
|
|
407
|
+
id: string;
|
|
408
|
+
name: string;
|
|
409
|
+
description: string;
|
|
410
|
+
/** The bare id a plugin's skill also answers to, while no one else claims it. */
|
|
411
|
+
alias?: string;
|
|
412
|
+
/** The plugin package that contributed it; absent for operator-installed skills. */
|
|
413
|
+
package?: string;
|
|
414
|
+
/** Where the SKILL.md lives. */
|
|
415
|
+
path?: string;
|
|
416
|
+
/** Ships with Stratus and is enabled for every agent, whatever its soul's `skills:` says. */
|
|
417
|
+
builtin?: boolean;
|
|
418
|
+
}
|
|
419
|
+
/** A plugin this daemon was asked to load, and what came of it. */
|
|
420
|
+
export interface GatewayPluginStatus {
|
|
421
|
+
package: string;
|
|
422
|
+
name?: string;
|
|
423
|
+
trusted?: boolean;
|
|
424
|
+
tools?: GatewayTool[];
|
|
425
|
+
skills?: GatewaySkill[];
|
|
426
|
+
/** Provider names it registered — what a soul's `provider:` can select. */
|
|
427
|
+
providers?: string[];
|
|
428
|
+
/** Channel kinds it registered, and the agents each carries. */
|
|
429
|
+
channels?: Array<{
|
|
430
|
+
kind: string;
|
|
431
|
+
agents: string[];
|
|
432
|
+
}>;
|
|
433
|
+
/** Memory store names it registered — what `memoryStore` can select. */
|
|
434
|
+
memory?: string[];
|
|
435
|
+
/** Executor names it registered — what `executor` can select. */
|
|
436
|
+
executors?: string[];
|
|
437
|
+
/** Present when the plugin did not load, and the only field that is. */
|
|
438
|
+
error?: string;
|
|
439
|
+
}
|
|
440
|
+
/** One provider a soul's `provider:` can select on this daemon. */
|
|
441
|
+
export interface GatewayProviderStatus {
|
|
442
|
+
/** The name as a soul writes it: `anthropic`, or a plugin's `ollama`. */
|
|
443
|
+
name: string;
|
|
444
|
+
/** The plugin package that registered it; absent for a built-in. */
|
|
445
|
+
package?: string;
|
|
446
|
+
}
|
|
447
|
+
export interface GatewayOptions {
|
|
448
|
+
env?: StateEnvironment;
|
|
449
|
+
/** Gateway-wide provider/model overrides applied beneath per-soul pins. */
|
|
450
|
+
selection?: RuntimeSelection;
|
|
451
|
+
/** Channel adapters to run (Slack, …). Started on start(), stopped on stop(). */
|
|
452
|
+
channels?: GatewayChannelAdapter[];
|
|
453
|
+
/**
|
|
454
|
+
* Which (kind, agent) pairs the host's own adapters carry, so a plugin
|
|
455
|
+
* channel claiming one of them is refused at load rather than started
|
|
456
|
+
* beside it. The Slack adapter the CLI wires reports its agents here;
|
|
457
|
+
* a host that omits this lets a plugin double up on a host adapter.
|
|
458
|
+
*/
|
|
459
|
+
hostChannelClaims?: Array<{
|
|
460
|
+
kind: string;
|
|
461
|
+
agents: readonly string[];
|
|
462
|
+
}>;
|
|
463
|
+
/**
|
|
464
|
+
* What an agent whose soul has no `tools:` list may call; see
|
|
465
|
+
* `AgentRunnerOptions.unlistedTools`. Omitted, the gateway asks the home
|
|
466
|
+
* (`unlistedToolsFor`): every tool on one an older build set up, safe
|
|
467
|
+
* tools only on a new one.
|
|
468
|
+
*/
|
|
469
|
+
unlistedTools?: UnlistedTools;
|
|
470
|
+
/**
|
|
471
|
+
* The policy, or a factory that builds one from the gateway's approval
|
|
472
|
+
* transport. Pass a factory whenever the policy parks turns on a human
|
|
473
|
+
* reached through a channel — `remote` mode — since that policy needs the
|
|
474
|
+
* transport the gateway is still constructing.
|
|
475
|
+
*
|
|
476
|
+
* Omitted, the runners get core's default: only `safe` tools run, and
|
|
477
|
+
* every other call is refused with an error naming this option. Pass
|
|
478
|
+
* `AllowAllApprovalPolicy` from `@stratusagent/core` to run every call.
|
|
479
|
+
*/
|
|
480
|
+
approvals?: ApprovalPolicy | ((transport: ApprovalTransport) => ApprovalPolicy);
|
|
481
|
+
/**
|
|
482
|
+
* How long a parked call waits before it denies itself. 0 waits forever,
|
|
483
|
+
* which is only ever right in a test. Default 15 minutes.
|
|
484
|
+
*/
|
|
485
|
+
approvalTimeoutMs?: number;
|
|
486
|
+
/**
|
|
487
|
+
* How many tool turns one dispatched turn may take before the runner
|
|
488
|
+
* wraps it up with a summary. Default 40 (`DEFAULT_MAX_TURNS` in core).
|
|
489
|
+
*
|
|
490
|
+
* Applies to every runner this gateway builds, so a delegated
|
|
491
|
+
* sub-session is held to the same ceiling as the turn that delegated it
|
|
492
|
+
* — they are separate dispatches and each gets its own allowance, not a
|
|
493
|
+
* share of one. `stratus serve` fills it from the trusted config's
|
|
494
|
+
* `maxTurns`.
|
|
495
|
+
*/
|
|
496
|
+
maxTurns?: number;
|
|
497
|
+
/**
|
|
498
|
+
* One agent's own budget, replacing `maxTurns` for its messages and the
|
|
499
|
+
* sub-sessions it delegates to that agent. Undefined means the shared one.
|
|
500
|
+
* `stratus serve` fills it from the trusted config's `agentMaxTurns`.
|
|
501
|
+
*/
|
|
502
|
+
maxTurnsFor?: (agentId: string) => number | undefined;
|
|
503
|
+
/**
|
|
504
|
+
* Whether an agent keeps working past its budget instead of wrapping up
|
|
505
|
+
* (`AgentRunnerOptions.autoContinue`): `true` without a cap, a number for
|
|
506
|
+
* that many extra allowances per message, undefined for off. `stratus
|
|
507
|
+
* serve` fills it from the trusted config's `autoContinue`.
|
|
508
|
+
*/
|
|
509
|
+
autoContinueFor?: (agentId: string) => true | number | undefined;
|
|
510
|
+
/**
|
|
511
|
+
* The activity watchdog: abort a turn when no event for its session has
|
|
512
|
+
* arrived for this long. Progress-based, not wall-clock — any delta, tool
|
|
513
|
+
* event, or response resets it. 0 disables. Default 120s.
|
|
514
|
+
*/
|
|
515
|
+
idleTimeoutMs?: number;
|
|
516
|
+
/**
|
|
517
|
+
* The events listener's sources and body limit (card 590), from the
|
|
518
|
+
* trusted config's `events` block. With no sources the event store is
|
|
519
|
+
* never opened, and every delivery is to an unknown source.
|
|
520
|
+
*/
|
|
521
|
+
events?: {
|
|
522
|
+
sources?: Record<string, EventSourceConfig>;
|
|
523
|
+
maxBodyBytes?: number;
|
|
524
|
+
};
|
|
525
|
+
/**
|
|
526
|
+
* Where the per-agent session stores (`agents/<id>/sessions.db`) and the
|
|
527
|
+
* fleet database (`fleet.db`, holding the schedules and the session
|
|
528
|
+
* index) live. Default `~/.stratus`.
|
|
529
|
+
*
|
|
530
|
+
* A directory rather than the single database path it replaced: after
|
|
531
|
+
* per-agent state there is no one file to point at, which is the
|
|
532
|
+
* point — a store is opened on one agent's path, so no query can return
|
|
533
|
+
* another agent's rows.
|
|
534
|
+
*/
|
|
535
|
+
stateDir?: string;
|
|
536
|
+
/**
|
|
537
|
+
* Scheduler limits — the interval floor, the per-agent cap on concurrent
|
|
538
|
+
* scheduled turns, and the tick cadence. Defaults are production values;
|
|
539
|
+
* tests lower them.
|
|
540
|
+
*/
|
|
541
|
+
schedules?: SchedulerLimits;
|
|
542
|
+
/**
|
|
543
|
+
* Plugins to load, keyed by package name — the `plugins` block, which the
|
|
544
|
+
* caller has already read from a **trusted** config. The gateway does not
|
|
545
|
+
* go looking for one: a plugin runs in-process with the daemon, and which
|
|
546
|
+
* file may say so is a decision that belongs where the config precedence
|
|
547
|
+
* is already understood.
|
|
548
|
+
*/
|
|
549
|
+
plugins?: PluginsConfig;
|
|
550
|
+
/**
|
|
551
|
+
* Which executor runs tool calls, by the name a plugin registered it
|
|
552
|
+
* under (`local`, or absent, is the built-in child-process executor).
|
|
553
|
+
* From a **trusted** config only, like `plugins`: an executor is where
|
|
554
|
+
* an agent's commands run, and a cloned repository swapping a sandbox
|
|
555
|
+
* for the host is the downgrade that rule exists to refuse. A name no
|
|
556
|
+
* loaded plugin registered fails `start()` — a daemon silently running
|
|
557
|
+
* commands on the host when the operator selected a sandbox is worse
|
|
558
|
+
* than one that did not start.
|
|
559
|
+
*/
|
|
560
|
+
executor?: string;
|
|
561
|
+
/**
|
|
562
|
+
* Host-owned limits for sub-agents (roadmap 24). `maxConcurrentPerTurn`
|
|
563
|
+
* caps how many one parent turn runs at once; the rest wait for a slot.
|
|
564
|
+
* It must be a whole number of at least 1, or `createGateway` throws.
|
|
565
|
+
*/
|
|
566
|
+
subAgents?: {
|
|
567
|
+
maxConcurrentPerTurn?: number;
|
|
568
|
+
};
|
|
569
|
+
/**
|
|
570
|
+
* Which store backs agent memory, by registered name (`file`, or absent,
|
|
571
|
+
* is the built-in). Same trust rule and same refusal as `executor`: a
|
|
572
|
+
* daemon writing memories into a store the operator did not select is a
|
|
573
|
+
* daemon whose agents remember into the wrong place.
|
|
574
|
+
*/
|
|
575
|
+
memoryStore?: string;
|
|
576
|
+
/**
|
|
577
|
+
* How plugin packages are resolved and imported.
|
|
578
|
+
*
|
|
579
|
+
* Worth passing, and `stratus serve` does. `import.meta.resolve` answers
|
|
580
|
+
* relative to the module that calls it, so the default here asks "is this
|
|
581
|
+
* plugin visible from the gateway package" when the question is "is it
|
|
582
|
+
* visible from the thing the operator installed". Those differ under any
|
|
583
|
+
* layout that is not flat — a pnpm install being the obvious one — and the
|
|
584
|
+
* failure is a plugin reported as not installed while sitting right next
|
|
585
|
+
* to the CLI.
|
|
586
|
+
*/
|
|
587
|
+
pluginHost?: OptionalModuleHost;
|
|
588
|
+
/**
|
|
589
|
+
* How this daemon comes back after `restart()`: called once the gateway
|
|
590
|
+
* has stopped, by the host that owns the process — `stratus serve`
|
|
591
|
+
* starts a fresh daemon and supervises it. The gateway can drain and
|
|
592
|
+
* stop; only its host can start a process. A host that omits this gives
|
|
593
|
+
* up `restart()`, which then throws `RestartUnsupportedError`.
|
|
594
|
+
*/
|
|
595
|
+
onRestart?: (outcome: RestartOutcome) => void | Promise<void>;
|
|
596
|
+
/**
|
|
597
|
+
* Asked by {@link Gateway.checkRestart} before a restart is announced,
|
|
598
|
+
* while this daemon still serves: a throw refuses the restart with its
|
|
599
|
+
* message and leaves the daemon up. `stratus serve` checks here that the
|
|
600
|
+
* trusted config will load, because the replacement refuses to start on
|
|
601
|
+
* one that will not (#214) — and a restart that drained first would turn
|
|
602
|
+
* a half-saved file into an outage. A host that omits this restarts
|
|
603
|
+
* unchecked.
|
|
604
|
+
*/
|
|
605
|
+
restartPreflight?: () => Promise<void>;
|
|
606
|
+
/**
|
|
607
|
+
* Start held: migrate, load the roster, and bring up only the channels
|
|
608
|
+
* that serve while held (the control API), so the daemon answers health
|
|
609
|
+
* and nothing else. No conversation channel connects, no schedule fires,
|
|
610
|
+
* no parked approval is re-asked and no abandoned turn is failed, and a
|
|
611
|
+
* dispatch is refused, until `promote()`. For an upgrade that has to prove
|
|
612
|
+
* the new version healthy before it touches any work.
|
|
613
|
+
*/
|
|
614
|
+
held?: boolean;
|
|
615
|
+
/**
|
|
616
|
+
* Called once after a held gateway is promoted and its work has begun.
|
|
617
|
+
* `stratus serve --hold-new-version` records the running version here,
|
|
618
|
+
* so the next start of the same version is not held. A rejection is
|
|
619
|
+
* warned about and does not undo the promotion: the daemon is serving.
|
|
620
|
+
*/
|
|
621
|
+
onPromoted?: () => Promise<void>;
|
|
622
|
+
/** The drain window a `restart()` uses when the request names none. Default 30s. */
|
|
623
|
+
restartDrainTimeoutMs?: number;
|
|
624
|
+
/**
|
|
625
|
+
* How long a roster reload waits on one channel's `reload()` before it
|
|
626
|
+
* goes on without it. Default 30s. The reload is not cancelled: the
|
|
627
|
+
* channel finishes on its own, and its outcome is logged when it does.
|
|
628
|
+
*/
|
|
629
|
+
channelReloadTimeoutMs?: number;
|
|
630
|
+
/** The drain window a `stop()` uses. Default 30s; see `DEFAULT_STOP_DRAIN_TIMEOUT_MS`. */
|
|
631
|
+
stopDrainTimeoutMs?: number;
|
|
632
|
+
log?: (line: string) => void;
|
|
633
|
+
warn?: (line: string) => void;
|
|
634
|
+
}
|
|
635
|
+
export interface DispatchInput {
|
|
636
|
+
/**
|
|
637
|
+
* Stable session id chosen by the caller — channels derive it from the
|
|
638
|
+
* conversation (e.g. a Slack thread), so any later message with the same
|
|
639
|
+
* id resumes the conversation, across daemon restarts included.
|
|
640
|
+
*/
|
|
641
|
+
sessionId: string;
|
|
642
|
+
/** Roster agent id. Defaults to the gateway's default agent. */
|
|
643
|
+
agentId?: string;
|
|
644
|
+
userMessage: string;
|
|
645
|
+
/** Images sent with the message — see `Message.images` in `@stratusagent/core`. */
|
|
646
|
+
images?: ImageAttachment[];
|
|
647
|
+
/**
|
|
648
|
+
* Whether the message was said to the agent — see `RunInput.addressed`
|
|
649
|
+
* in `@stratusagent/core`. `false` runs a turn the agent may answer with
|
|
650
|
+
* nothing; omitted, the turn is one somebody asked for.
|
|
651
|
+
*/
|
|
652
|
+
addressed?: boolean;
|
|
653
|
+
/**
|
|
654
|
+
* Said to the agent, but possibly needing no answer — see
|
|
655
|
+
* `RunInput.replyOptional` in `@stratusagent/core` (card 601). Messages
|
|
656
|
+
* merged into one turn (card 423) keep it only if every one of them
|
|
657
|
+
* carries it: one that owes an answer makes the turn owe it.
|
|
658
|
+
*/
|
|
659
|
+
replyOptional?: boolean;
|
|
660
|
+
/**
|
|
661
|
+
* What was said before this message that the agent never heard — see
|
|
662
|
+
* `RunInput.earlier` in `@stratusagent/core`. Used only when this
|
|
663
|
+
* dispatch creates the session; a session that already exists ignores
|
|
664
|
+
* it, so a caller that cannot tell which case it is in may always send it.
|
|
665
|
+
*/
|
|
666
|
+
earlier?: ObserveEntry[];
|
|
667
|
+
metadata?: JsonObject;
|
|
668
|
+
signal?: AbortSignal;
|
|
669
|
+
/**
|
|
670
|
+
* Caller-chosen id for this turn, so events emitted while it runs can be
|
|
671
|
+
* attributed to it. A session processes several turns in sequence and
|
|
672
|
+
* `StratusEvent` carries no turn identifier, so a surface that queued a
|
|
673
|
+
* message has no other way to tell its own deltas from the next caller's.
|
|
674
|
+
*
|
|
675
|
+
* Optional because most callers do not need one: a channel renders
|
|
676
|
+
* whatever arrives for the conversation it is watching.
|
|
677
|
+
*/
|
|
678
|
+
turnId?: string;
|
|
679
|
+
/**
|
|
680
|
+
* The caller's name for this message, unique within the session — for a
|
|
681
|
+
* channel, the platform's own message id. A repeat never starts a second
|
|
682
|
+
* turn: while the first is queued or running the repeat resolves with it;
|
|
683
|
+
* once it has finished the repeat resolves with the session as it stands,
|
|
684
|
+
* running nothing and emitting nothing — which may have moved on since,
|
|
685
|
+
* so the repeated message's own reply is `turnReplyFor(session, key)`,
|
|
686
|
+
* not `latestTurnReply`; and a turn whose process died
|
|
687
|
+
* mid-run is continued, once (`AgentRunner.continueTurn`), by whichever
|
|
688
|
+
* comes first — the repeat or the next start's sweep. Not on a harness
|
|
689
|
+
* (`runsOnHarness`), whose own tool loop may already have acted on the
|
|
690
|
+
* prompt: that turn is failed, as an unkeyed one is.
|
|
691
|
+
*
|
|
692
|
+
* Stored on the user message in the write that accepts it
|
|
693
|
+
* (`Message.idempotencyKey`), so it lasts as long as the transcript does.
|
|
694
|
+
* A repeat's other fields are not compared, and its `signal` does not
|
|
695
|
+
* reach a turn it attaches to; a failed turn stays failed, and retrying
|
|
696
|
+
* it takes a new key.
|
|
697
|
+
*
|
|
698
|
+
* Optional, and a host that omits it gives up crash-safe dedupe: a
|
|
699
|
+
* channel that delivers at least once can only dedupe in memory, which a
|
|
700
|
+
* restart erases, and a turn that was running when the process died is
|
|
701
|
+
* failed at the next start instead of finished.
|
|
702
|
+
*/
|
|
703
|
+
idempotencyKey?: string;
|
|
704
|
+
/**
|
|
705
|
+
* Called, before the dispatch resolves, when it repeats a turn rather
|
|
706
|
+
* than starting or continuing one, and says which kind of repeat:
|
|
707
|
+
*
|
|
708
|
+
* - `live` — it attached to a turn another dispatch with the same key
|
|
709
|
+
* started in this process and is still waiting on. That dispatch is
|
|
710
|
+
* alive to render the outcome, so a caller with a reply of its own in
|
|
711
|
+
* progress takes it down and posts nothing.
|
|
712
|
+
* - `finished` — the turn is over and nothing ran. Its outcome may never
|
|
713
|
+
* have been posted — the process can die between the turn's last write
|
|
714
|
+
* and the channel's — and nothing durable says whether it was, so the
|
|
715
|
+
* caller posts it from the session: `turnReplyFor`, `turnFilesFor`, and
|
|
716
|
+
* `turnFailureFor` from `@stratusagent/core`, since the session may
|
|
717
|
+
* have moved on and no event will carry any of it. Said twice beats
|
|
718
|
+
* never said.
|
|
719
|
+
*
|
|
720
|
+
* Not called for a repeat that continues a turn a crash left unfinished:
|
|
721
|
+
* that turn runs, its events are this caller's, and nobody else is
|
|
722
|
+
* rendering it.
|
|
723
|
+
*/
|
|
724
|
+
onRepeat?: (repeat: 'live' | 'finished') => void;
|
|
725
|
+
/**
|
|
726
|
+
* Opts this dispatch into merged turns. A message that arrives while the
|
|
727
|
+
* session's turn is running is not given a turn of its own: every
|
|
728
|
+
* opted-in, addressed dispatch queued by the time that turn ends is
|
|
729
|
+
* answered by ONE next turn, its messages joined in arrival order in one
|
|
730
|
+
* user message (`Message.absorbedKeys` keeps each one's key, so a repeat
|
|
731
|
+
* is that turn). No delay is added; the batch is whatever queued.
|
|
732
|
+
*
|
|
733
|
+
* The first queued dispatch leads: its `turnId` stamps the merged turn's
|
|
734
|
+
* events, and it resolves as any dispatch does. Each later one is
|
|
735
|
+
* absorbed — called here, before it resolves (or rejects) with the same
|
|
736
|
+
* outcome — and, like a `live` repeat, posts nothing of its own: the
|
|
737
|
+
* leader's caller renders the answer. A caller that cannot withdraw what
|
|
738
|
+
* it put up leaves this unset and keeps one turn per dispatch.
|
|
739
|
+
*
|
|
740
|
+
* The merged turn runs at the least trusted sender in it. A turn nobody
|
|
741
|
+
* asked for (`addressed: false`) is never merged.
|
|
742
|
+
*
|
|
743
|
+
* Steering (card 948): while the running turn is itself an opted-in,
|
|
744
|
+
* addressed dispatch for the same agent, the batch waiting behind it is
|
|
745
|
+
* handed to that turn at its next tool step instead (`RunInput.steer`),
|
|
746
|
+
* so the agent reads it before its next call rather than after its whole
|
|
747
|
+
* turn. Every dispatch in the batch, its leader included, is then
|
|
748
|
+
* absorbed into the running turn — called here and settled with that
|
|
749
|
+
* turn's outcome — and the running turn's caller renders the answer. A
|
|
750
|
+
* turn that answers without another tool step leaves the batch to run
|
|
751
|
+
* next, merged, as above.
|
|
752
|
+
*/
|
|
753
|
+
onAbsorbed?: () => void;
|
|
754
|
+
}
|
|
755
|
+
export interface ObserveInput {
|
|
756
|
+
sessionId: string;
|
|
757
|
+
/** Must match the session's agent when given; a session never crosses identities. */
|
|
758
|
+
agentId?: string;
|
|
759
|
+
/** What was said, speaker included — see `ObserveInput.message` in core. */
|
|
760
|
+
message: string;
|
|
761
|
+
/** This message's metadata, read for the speaker's trust; never merged. */
|
|
762
|
+
metadata?: JsonObject;
|
|
763
|
+
}
|
|
764
|
+
export interface Gateway {
|
|
765
|
+
start(): Promise<void>;
|
|
766
|
+
stop(): Promise<void>;
|
|
767
|
+
/** The one entrypoint: resolve the agent, load-or-create the session, run a turn. */
|
|
768
|
+
dispatch(input: DispatchInput): Promise<Session>;
|
|
769
|
+
/**
|
|
770
|
+
* A message into an existing session with no turn run on it —
|
|
771
|
+
* `AgentRunner.observe`, on the session's chain so an overheard message
|
|
772
|
+
* and a turn never interleave writes, and behind the same two refusals
|
|
773
|
+
* as `dispatch`: the scheduler's reserved id namespace and the daemon's
|
|
774
|
+
* reserved metadata keys, since text entering a session's context is
|
|
775
|
+
* text entering its next prompt whichever door it came through.
|
|
776
|
+
*
|
|
777
|
+
* Existing only: an agent overhears a conversation it is already in, and
|
|
778
|
+
* a session that would have to be created is one it was never invited
|
|
779
|
+
* to — so a session that does not exist is nothing to hear into, and
|
|
780
|
+
* resolves `undefined` rather than refusing. Read on the chain, which is
|
|
781
|
+
* what makes that answer right: an invitation still in flight — a first
|
|
782
|
+
* mention whose dispatch is queued ahead of this — has created the
|
|
783
|
+
* session by the time this runs, where a read outside the chain would
|
|
784
|
+
* have found nothing and dropped the message for good. A rolled-over
|
|
785
|
+
* transcript refuses like a dispatch would.
|
|
786
|
+
*/
|
|
787
|
+
observe(input: ObserveInput): Promise<Session | undefined>;
|
|
788
|
+
/** Live events from every runner, one stream for all consumers. */
|
|
789
|
+
readonly bus: EventBus;
|
|
790
|
+
/** The store shared by every runner (durable across restarts). */
|
|
791
|
+
readonly store: ShardedSessionStore;
|
|
792
|
+
/** The current roster, default agent included. */
|
|
793
|
+
agents(): AgentDefinition[];
|
|
794
|
+
/**
|
|
795
|
+
* Re-read the agents directory and the configured default soul, returning
|
|
796
|
+
* the roster that results. Souls are already re-read on every dispatch, so
|
|
797
|
+
* this is for files that appeared or disappeared since start — an agent
|
|
798
|
+
* created by another surface, or one whose soul was deleted.
|
|
799
|
+
*/
|
|
800
|
+
reloadRoster(): Promise<AgentDefinition[]>;
|
|
801
|
+
/**
|
|
802
|
+
* `reloadRoster()`, then what each started channel that can reload
|
|
803
|
+
* (`ChannelAdapter.reload`) did with it, in start order: where a secret
|
|
804
|
+
* stored while the daemon runs is picked up without a restart. A channel
|
|
805
|
+
* missing from the list can't reload, and reads its secrets at the next
|
|
806
|
+
* start. Rejects as `reloadRoster()` does, when the roster won't load.
|
|
807
|
+
*/
|
|
808
|
+
reloadRosterAndChannels(): Promise<{
|
|
809
|
+
agents: AgentDefinition[];
|
|
810
|
+
channels: ChannelReloadOutcome[];
|
|
811
|
+
}>;
|
|
812
|
+
/**
|
|
813
|
+
* Re-read `~/.stratus/skills/` and serve what is there now, without a
|
|
814
|
+
* restart — the shape `reloadRoster()` established: idempotent,
|
|
815
|
+
* serialized against itself and the roster reload, and a failure that
|
|
816
|
+
* leaves the previous set serving. Plugin-contributed skills are rebuilt
|
|
817
|
+
* from the plugins already loaded (never re-imported), in the same order
|
|
818
|
+
* they loaded, so operator precedence and contested aliases come out as
|
|
819
|
+
* a restart would produce them.
|
|
820
|
+
*
|
|
821
|
+
* A skill that would not load — a malformed `SKILL.md`, a directory whose
|
|
822
|
+
* name is not an id — rejects with the file named and changes nothing:
|
|
823
|
+
* half a catalog is worse than a stale one.
|
|
824
|
+
*
|
|
825
|
+
* Loaded is not enabled: a reloaded skill still reaches only the agents
|
|
826
|
+
* whose souls list it.
|
|
827
|
+
*/
|
|
828
|
+
reloadSkills(): Promise<GatewaySkill[]>;
|
|
829
|
+
/**
|
|
830
|
+
* Announce a restart: refuse new turns now, let running ones finish
|
|
831
|
+
* inside the drain window, abort what is still running at the end of
|
|
832
|
+
* it, stop, and hand the process back to the host through `onRestart`.
|
|
833
|
+
* Returns at once with the terms; the drain runs behind it. Idempotent —
|
|
834
|
+
* a second call while one is underway reports the same announcement.
|
|
835
|
+
*
|
|
836
|
+
* What comes back is what a stop-and-start brings back: durable sessions,
|
|
837
|
+
* schedules, and channels; a call parked on a human is denied as
|
|
838
|
+
* cancelled, exactly as a stop denies it. The refusal comes first, which
|
|
839
|
+
* makes the drain's known gap smaller — a turn finishing after the
|
|
840
|
+
* snapshot can still start work the snapshot never saw — but does not
|
|
841
|
+
* close it.
|
|
842
|
+
*/
|
|
843
|
+
restart(request?: RestartRequest): RestartStatus;
|
|
844
|
+
/**
|
|
845
|
+
* Whether a `restart()` now would bring the daemon back, as the host
|
|
846
|
+
* judges it (`restartPreflight`): rejects with the host's refusal,
|
|
847
|
+
* resolves when there is none. Separate from `restart()`, which stays
|
|
848
|
+
* synchronous and idempotent, so a caller asks this first — `POST
|
|
849
|
+
* /restart` does.
|
|
850
|
+
*/
|
|
851
|
+
checkRestart(): Promise<void>;
|
|
852
|
+
/**
|
|
853
|
+
* The turn currently running on a session, if the caller that started it
|
|
854
|
+
* named one. Single-flight per session is what makes this exact: at most
|
|
855
|
+
* one turn is ever running on a session, so an event emitted for it
|
|
856
|
+
* belongs to this turn and no other.
|
|
857
|
+
*/
|
|
858
|
+
activeTurnId(sessionId: string): string | undefined;
|
|
859
|
+
/**
|
|
860
|
+
* True while a side run is running under this session id: a reviewer
|
|
861
|
+
* call (`review:…`) or a memory pass (`memory-pass:…`). Its events reach
|
|
862
|
+
* `bus` like a turn's, so whatever counts provider calls there counts
|
|
863
|
+
* them, but it is not a conversation: nobody can send to it, it is never
|
|
864
|
+
* stored, and a surface that lists conversations leaves it out. Known by
|
|
865
|
+
* the run, not the id's shape, so a dispatched session whose id happens
|
|
866
|
+
* to start the same way is still a conversation.
|
|
867
|
+
*/
|
|
868
|
+
isSideRun(sessionId: string): boolean;
|
|
869
|
+
/**
|
|
870
|
+
* Aborts every turn running on a session right now, one recovered after a
|
|
871
|
+
* restart included, the way the watchdog and a restart's window do: each
|
|
872
|
+
* fails with `reason`, its session saved. Returns how many turns are on
|
|
873
|
+
* the session, each now stopping: one aborted earlier that has not
|
|
874
|
+
* settled yet still counts (it is not aborted again), so 0 means nothing
|
|
875
|
+
* is running there at all. An idle or unknown session is 0 and nothing
|
|
876
|
+
* happens. A message queued on the session but not started is left to
|
|
877
|
+
* its caller's own signal. The caller decides who may stop; this asks
|
|
878
|
+
* nothing.
|
|
879
|
+
*/
|
|
880
|
+
abortTurns(sessionId: string, reason: string): number;
|
|
881
|
+
/** Every call parked on a human right now, oldest first. */
|
|
882
|
+
pendingApprovals(): PendingApproval[];
|
|
883
|
+
/**
|
|
884
|
+
* Every schedule the fleet has set, oldest first — the audit list the
|
|
885
|
+
* spec demands: an agent that scheduled something an operator cannot see
|
|
886
|
+
* or stop is a bug. Each row is also the scope of its own destination
|
|
887
|
+
* grant, so this doubles as the list of standing permissions to speak.
|
|
888
|
+
*/
|
|
889
|
+
schedules(): ScheduleRecord[];
|
|
890
|
+
/**
|
|
891
|
+
* Cancel any agent's schedule — the operator's stop button, and the
|
|
892
|
+
* revocation of the destination grant riding on the row. False when no
|
|
893
|
+
* such schedule exists.
|
|
894
|
+
*/
|
|
895
|
+
cancelSchedule(scheduleId: string): boolean;
|
|
896
|
+
/**
|
|
897
|
+
* One request to the events port (card 590): verified, deduplicated,
|
|
898
|
+
* matched against event triggers and recorded, all before the answer.
|
|
899
|
+
* The listener in control-api calls this with the raw body.
|
|
900
|
+
*/
|
|
901
|
+
receiveEvent(request: EventIntakeRequest): Promise<EventIntakeResponse>;
|
|
902
|
+
/**
|
|
903
|
+
* Per configured event source: the last delivery, the last refusal and
|
|
904
|
+
* why, and the last 24 hours by status, with the requests refused before
|
|
905
|
+
* they could leave a row (`stratus events`, `/health`). No sources when
|
|
906
|
+
* none is configured.
|
|
907
|
+
*/
|
|
908
|
+
eventSummary(): EventsSummary;
|
|
909
|
+
/** Delivery rows, newest first; a row never holds the body. Empty when no source is configured. */
|
|
910
|
+
eventDeliveries(options?: {
|
|
911
|
+
source?: string;
|
|
912
|
+
limit?: number;
|
|
913
|
+
}): EventDeliveryRow[];
|
|
914
|
+
/**
|
|
915
|
+
* An agent's memory passes from its ledger, newest first (spec 962).
|
|
916
|
+
* Read from the file each time, so a pass that just ended shows.
|
|
917
|
+
*/
|
|
918
|
+
memoryPasses(agentId: string): Promise<MemoryPassSummary[]>;
|
|
919
|
+
/**
|
|
920
|
+
* One pass and the facts it wrote, from the store this daemon writes
|
|
921
|
+
* to (in shadow, the facts it would have written). Undefined when the
|
|
922
|
+
* ledger has no such pass.
|
|
923
|
+
*/
|
|
924
|
+
memoryPass(agentId: string, passId: string): Promise<MemoryPassDetail | undefined>;
|
|
925
|
+
/**
|
|
926
|
+
* Every registered tool, with the risk it will actually be judged at and
|
|
927
|
+
* the package it came from.
|
|
928
|
+
*
|
|
929
|
+
* Provenance is the reason this is not `tools.describe()`: "which of
|
|
930
|
+
* these can this daemon run, and whose code is it" is the question the
|
|
931
|
+
* catalog endpoint and 07's tool screens are asking, and a descriptor
|
|
932
|
+
* list answers only half of it. Kernel tools have no package, which is
|
|
933
|
+
* itself the answer for them.
|
|
934
|
+
*/
|
|
935
|
+
tools(): GatewayTool[];
|
|
936
|
+
/**
|
|
937
|
+
* Every skill this daemon serves — operator-installed and
|
|
938
|
+
* plugin-contributed — with the ids an allowlist can name. Descriptors
|
|
939
|
+
* only: bodies stay behind `skill.read`, on the agent's own turn.
|
|
940
|
+
*/
|
|
941
|
+
skills(): GatewaySkill[];
|
|
942
|
+
/**
|
|
943
|
+
* The plugins this daemon loaded, and the ones it could not — a plugin an
|
|
944
|
+
* operator enabled that failed to load is exactly what they need to see,
|
|
945
|
+
* and an empty list would say the opposite.
|
|
946
|
+
*/
|
|
947
|
+
plugins(): GatewayPluginStatus[];
|
|
948
|
+
/**
|
|
949
|
+
* Every provider a soul's `provider:` can select on this daemon: the
|
|
950
|
+
* built-ins, then what loaded plugins registered, in load order. The
|
|
951
|
+
* registry, not the literal — a surface listing providers reads this so
|
|
952
|
+
* a contributed one is offered where the built-ins are.
|
|
953
|
+
*/
|
|
954
|
+
providers(): GatewayProviderStatus[];
|
|
955
|
+
/**
|
|
956
|
+
* The soul behind each agent in the current roster, built-in included
|
|
957
|
+
* (which has none).
|
|
958
|
+
*
|
|
959
|
+
* The roster the daemon is *serving*, which the agents directory stops
|
|
960
|
+
* describing the moment a soul is added, deleted, or broken since the last
|
|
961
|
+
* reload — and the gateway keeps dispatching from the soul it cached. A
|
|
962
|
+
* caller that has to answer "what would a turn as this agent run on" needs
|
|
963
|
+
* the pins that answer it, so this hands back the parsed soul rather than a
|
|
964
|
+
* path a second reader would have to re-read (and now read differently).
|
|
965
|
+
*
|
|
966
|
+
* Async because it re-reads each soul file, exactly as `refreshAgent` does
|
|
967
|
+
* before every dispatch: a pin edited on disk is live on the next turn, so
|
|
968
|
+
* a caller answering from the load-time snapshot would name the provider
|
|
969
|
+
* the daemon has already stopped billing. An unreadable file falls back to
|
|
970
|
+
* the cached soul, which is what that next dispatch falls back to as well.
|
|
971
|
+
*
|
|
972
|
+
* An agent is **absent** from this list when its file now declares a
|
|
973
|
+
* different id: `refreshAgent` refuses every dispatch for the old one until
|
|
974
|
+
* the roster reloads, so nothing is served under it and no runtime should
|
|
975
|
+
* be claimed for it. The roster it reports is therefore what can currently
|
|
976
|
+
* run, which is a narrower thing than `agents()`.
|
|
977
|
+
*/
|
|
978
|
+
servedSouls(): Promise<Array<{
|
|
979
|
+
id: string;
|
|
980
|
+
soul?: ParsedSoul;
|
|
981
|
+
}>>;
|
|
982
|
+
/**
|
|
983
|
+
* Where a durable session came from — its agent, and the metadata the
|
|
984
|
+
* dispatching surface attached to it.
|
|
985
|
+
*
|
|
986
|
+
* A projection rather than an accessor: the session's messages are not
|
|
987
|
+
* a channel's business, and handing back the whole record to save a few
|
|
988
|
+
* lines would make them one.
|
|
989
|
+
*/
|
|
990
|
+
sessionRouting(sessionId: string): Promise<SessionRouting | undefined>;
|
|
991
|
+
/**
|
|
992
|
+
* Whether a session already accepted the message an idempotency key names,
|
|
993
|
+
* as addressed to it: a turn was started for it, finished, running or
|
|
994
|
+
* still queued, here or in the transcript a recent rollover archived
|
|
995
|
+
* (`settleRepeat` reads the same). A turn nobody asked for
|
|
996
|
+
* (`addressed: false`) does not count — that agent heard the message, it
|
|
997
|
+
* did not take it. Lets a channel route a redelivered message to the agent
|
|
998
|
+
* that accepted it rather than to whoever its routing rule picks now.
|
|
999
|
+
*/
|
|
1000
|
+
holdsMessage(sessionId: string, idempotencyKey: string): Promise<boolean>;
|
|
1001
|
+
/**
|
|
1002
|
+
* Start a conversation over under the same id, leaving its transcript so
|
|
1003
|
+
* far behind as an archived session.
|
|
1004
|
+
*
|
|
1005
|
+
* The remedy for a session whose trust label cannot honestly be raised. A
|
|
1006
|
+
* session from before provenance was tracked reads `unknown`, the label
|
|
1007
|
+
* never rises, and some sessions never end — a Slack DM is one resumable
|
|
1008
|
+
* session for the life of the install — so without this an operator's
|
|
1009
|
+
* own conversation would write `unknown` forever, however carefully the
|
|
1010
|
+
* memory it injects was re-asserted. A session boundary is the only move
|
|
1011
|
+
* that neither strands the conversation nor quietly raises trust over a
|
|
1012
|
+
* transcript nobody re-read: the old session stays exactly what it was,
|
|
1013
|
+
* under `<id>:rolledover:<time>-<suffix>`, and the fresh one starts at the top of
|
|
1014
|
+
* the lattice with only the routing metadata the channel needs to keep
|
|
1015
|
+
* finding it. The fresh session is still `unknown` on its first turn if
|
|
1016
|
+
* the memory it injects is — correctly, and that is the case that
|
|
1017
|
+
* catches anyone who thinks the rollover alone is the remedy.
|
|
1018
|
+
*
|
|
1019
|
+
* Refuses a session with a turn in flight: the runner holds the live
|
|
1020
|
+
* object and would save the old transcript back over the fresh row.
|
|
1021
|
+
*/
|
|
1022
|
+
rolloverSession(sessionId: string): Promise<{
|
|
1023
|
+
sessionId: string;
|
|
1024
|
+
archivedAs: string;
|
|
1025
|
+
}>;
|
|
1026
|
+
/**
|
|
1027
|
+
* Settles a parked call. Returns false when the request is not pending —
|
|
1028
|
+
* already decided, expired, or belonging to a turn that was cancelled —
|
|
1029
|
+
* which is the normal outcome of a button clicked a minute too late, not
|
|
1030
|
+
* an error. Callers surface it to whoever clicked; they must never treat
|
|
1031
|
+
* it as "try again".
|
|
1032
|
+
*/
|
|
1033
|
+
resolveApproval(input: ResolveApprovalInput): boolean;
|
|
1034
|
+
/** Whether the daemon is taking new work. */
|
|
1035
|
+
intake(): IntakeState;
|
|
1036
|
+
/**
|
|
1037
|
+
* Stop taking new work without stopping. Turns already running or queued
|
|
1038
|
+
* finish; a new dispatch is refused with `IntakePausedError` carrying
|
|
1039
|
+
* `message` (or `DEFAULT_PAUSED_MESSAGE`); due recurring schedules are
|
|
1040
|
+
* skipped and one-shots wait. Channels stay connected, so they can say
|
|
1041
|
+
* so. Durable: the pause outlives a restart until `resumeIntake`.
|
|
1042
|
+
* Pausing again replaces the message and keeps the original `since`.
|
|
1043
|
+
*/
|
|
1044
|
+
pauseIntake(input?: {
|
|
1045
|
+
message?: string;
|
|
1046
|
+
}): Promise<IntakeState>;
|
|
1047
|
+
/** Take new work again. Idempotent. */
|
|
1048
|
+
resumeIntake(): Promise<IntakeState>;
|
|
1049
|
+
/** Whether the gateway started held and has not been promoted yet. */
|
|
1050
|
+
held(): boolean;
|
|
1051
|
+
/**
|
|
1052
|
+
* End a held start: connect the remaining channels, start the scheduler,
|
|
1053
|
+
* and run the recovery sweeps a normal start runs. Resolves once the
|
|
1054
|
+
* channels are up. A no-op on a gateway that is not held. A required
|
|
1055
|
+
* channel that fails to start rejects, and the gateway stays held.
|
|
1056
|
+
*/
|
|
1057
|
+
promote(): Promise<void>;
|
|
1058
|
+
/**
|
|
1059
|
+
* Answers a `credential.requested` with the value an approver entered:
|
|
1060
|
+
* stores it add-only (`addNamedCredential`), under the agent or the fleet
|
|
1061
|
+
* as the request asked, and adds the name to the requesting agent's soul.
|
|
1062
|
+
* The value goes to the credential store and nowhere else — not onto the
|
|
1063
|
+
* bus, the log, or the session.
|
|
1064
|
+
*
|
|
1065
|
+
* Who may answer is the channel's question, as it is for approvals.
|
|
1066
|
+
*/
|
|
1067
|
+
provideCredential(input: ProvideCredentialInput): Promise<ProvideCredentialResult>;
|
|
1068
|
+
}
|
|
1069
|
+
export interface ProvideCredentialInput {
|
|
1070
|
+
requestId: string;
|
|
1071
|
+
/**
|
|
1072
|
+
* As entered, line breaks included: a key file such as a PEM private key
|
|
1073
|
+
* is stored with the ones inside it. Line breaks at either end are
|
|
1074
|
+
* dropped, the way `stratus credential set` drops a trailing one.
|
|
1075
|
+
*/
|
|
1076
|
+
value: string;
|
|
1077
|
+
/** Who provided it. Channel-native id (a Slack user). */
|
|
1078
|
+
actor?: string;
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* `ok: false` carries a sentence for the person who entered the value: the
|
|
1082
|
+
* request is gone, the name is already stored, or the value is empty or a
|
|
1083
|
+
* PEM key whose line breaks were lost.
|
|
1084
|
+
* `granted: false` on success means the soul already listed the name.
|
|
1085
|
+
*
|
|
1086
|
+
* `retired: true` means the request is no longer pending and no answer
|
|
1087
|
+
* can change that, so a channel should take its form down: the name was
|
|
1088
|
+
* stored since (add-only means no retry can land), the soul went to another
|
|
1089
|
+
* agent, or the request was already answered. Without it the refusal is
|
|
1090
|
+
* one a corrected value can clear, like an empty one, and the form stays.
|
|
1091
|
+
*/
|
|
1092
|
+
export type ProvideCredentialResult = {
|
|
1093
|
+
ok: true;
|
|
1094
|
+
name: string;
|
|
1095
|
+
scope: CredentialScope;
|
|
1096
|
+
agentId: string;
|
|
1097
|
+
granted: boolean;
|
|
1098
|
+
} | {
|
|
1099
|
+
ok: false;
|
|
1100
|
+
message: string;
|
|
1101
|
+
retired?: boolean;
|
|
1102
|
+
};
|
|
1103
|
+
//# sourceMappingURL=types.d.ts.map
|