@autumn-dev/autumn-bus 0.1.0-next.2
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/LICENSE +201 -0
- package/README.md +60 -0
- package/dist/client.d.ts +48 -0
- package/dist/client.js +173 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +26 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +141 -0
- package/dist/protocol.js +2 -0
- package/dist/protocol.js.map +1 -0
- package/dist/session.d.ts +46 -0
- package/dist/session.js +176 -0
- package/dist/session.js.map +1 -0
- package/package.json +73 -0
- package/src/client.ts +255 -0
- package/src/errors.ts +38 -0
- package/src/index.ts +4 -0
- package/src/protocol.ts +161 -0
- package/src/session.ts +216 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { BusError } from './errors.js'
|
|
2
|
+
import type { BusErrorCode } from './errors.js'
|
|
3
|
+
import type {
|
|
4
|
+
Agent,
|
|
5
|
+
AgentLifecycle,
|
|
6
|
+
AskHumanInput,
|
|
7
|
+
BusHealth,
|
|
8
|
+
BusMessage,
|
|
9
|
+
BusTask,
|
|
10
|
+
CreateScopeInput,
|
|
11
|
+
CreateScopeResult,
|
|
12
|
+
DeliveryReceipt,
|
|
13
|
+
HumanEscalation,
|
|
14
|
+
InboxReservation,
|
|
15
|
+
RegisterAgentInput,
|
|
16
|
+
RegisterAgentResult,
|
|
17
|
+
SendMessageInput
|
|
18
|
+
} from './protocol.js'
|
|
19
|
+
|
|
20
|
+
interface Success<T> {
|
|
21
|
+
ok: true
|
|
22
|
+
result: T
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface Failure {
|
|
26
|
+
ok: false
|
|
27
|
+
error: { code: BusErrorCode; message: string; details?: Record<string, unknown> }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface OperationOptions {
|
|
31
|
+
signal?: AbortSignal
|
|
32
|
+
timeoutMs?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function request<T>(
|
|
36
|
+
address: string,
|
|
37
|
+
token: string | undefined,
|
|
38
|
+
method: string,
|
|
39
|
+
path: string,
|
|
40
|
+
value?: unknown,
|
|
41
|
+
options: OperationOptions = {}
|
|
42
|
+
): Promise<T> {
|
|
43
|
+
const timeoutMs = options.timeoutMs ?? 30_000
|
|
44
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
45
|
+
throw new BusError('INVALID_ARGUMENT', 'timeoutMs must be a positive finite number')
|
|
46
|
+
}
|
|
47
|
+
const controller = new AbortController()
|
|
48
|
+
const onAbort = () => controller.abort(options.signal?.reason ?? new Error('Operation aborted'))
|
|
49
|
+
if (options.signal?.aborted) onAbort()
|
|
50
|
+
else options.signal?.addEventListener('abort', onAbort, { once: true })
|
|
51
|
+
const timeout = setTimeout(
|
|
52
|
+
() => controller.abort(new Error(`Autumn Bus request timed out after ${timeoutMs}ms`)),
|
|
53
|
+
timeoutMs
|
|
54
|
+
)
|
|
55
|
+
let response: Response
|
|
56
|
+
let text: string
|
|
57
|
+
try {
|
|
58
|
+
response = await fetch(`${address}${path}`, {
|
|
59
|
+
method,
|
|
60
|
+
headers: {
|
|
61
|
+
accept: 'application/json',
|
|
62
|
+
...(value === undefined ? {} : { 'content-type': 'application/json' }),
|
|
63
|
+
...(token === undefined ? {} : { authorization: `Bearer ${token}` })
|
|
64
|
+
},
|
|
65
|
+
...(value === undefined ? {} : { body: JSON.stringify(value) }),
|
|
66
|
+
signal: controller.signal
|
|
67
|
+
})
|
|
68
|
+
text = await response.text()
|
|
69
|
+
} catch (error) {
|
|
70
|
+
const cause = controller.signal.aborted ? controller.signal.reason : error
|
|
71
|
+
const message = cause instanceof Error ? cause.message : 'Network request failed'
|
|
72
|
+
throw new BusError('INTERNAL', `Autumn Bus request failed: ${message}`)
|
|
73
|
+
} finally {
|
|
74
|
+
clearTimeout(timeout)
|
|
75
|
+
options.signal?.removeEventListener('abort', onAbort)
|
|
76
|
+
}
|
|
77
|
+
let payload: Success<T> | Failure | T
|
|
78
|
+
try {
|
|
79
|
+
payload = JSON.parse(text) as Success<T> | Failure | T
|
|
80
|
+
} catch {
|
|
81
|
+
throw new BusError('INTERNAL', `Autumn Bus returned a non-JSON response with HTTP ${response.status}`)
|
|
82
|
+
}
|
|
83
|
+
if (response.ok) {
|
|
84
|
+
if (payload && typeof payload === 'object' && 'ok' in payload && payload.ok === true)
|
|
85
|
+
return (payload as Success<T>).result
|
|
86
|
+
return payload as T
|
|
87
|
+
}
|
|
88
|
+
if (
|
|
89
|
+
payload &&
|
|
90
|
+
typeof payload === 'object' &&
|
|
91
|
+
'ok' in payload &&
|
|
92
|
+
payload.ok === false &&
|
|
93
|
+
'error' in payload &&
|
|
94
|
+
payload.error &&
|
|
95
|
+
typeof payload.error === 'object' &&
|
|
96
|
+
'code' in payload.error &&
|
|
97
|
+
'message' in payload.error
|
|
98
|
+
) {
|
|
99
|
+
const failure = payload as Failure
|
|
100
|
+
throw new BusError(failure.error.code, failure.error.message, failure.error.details)
|
|
101
|
+
}
|
|
102
|
+
throw new BusError('INTERNAL', `Autumn Bus request failed with HTTP ${response.status}`)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export class AutumnBusAdminClient {
|
|
106
|
+
constructor(
|
|
107
|
+
readonly address: string,
|
|
108
|
+
private readonly adminToken: string
|
|
109
|
+
) {}
|
|
110
|
+
|
|
111
|
+
health(options?: OperationOptions): Promise<BusHealth> {
|
|
112
|
+
return request(this.address, undefined, 'GET', '/health', undefined, options)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
createScope(input: CreateScopeInput = {}, options?: OperationOptions): Promise<CreateScopeResult> {
|
|
116
|
+
return request(this.address, this.adminToken, 'POST', '/v1/scopes', input, options)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async shutdown(options?: OperationOptions): Promise<void> {
|
|
120
|
+
await request(this.address, this.adminToken, 'POST', '/v1/admin/shutdown', {}, options)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export class AutumnBusScopeClient {
|
|
125
|
+
constructor(
|
|
126
|
+
readonly address: string,
|
|
127
|
+
readonly scopeToken: string
|
|
128
|
+
) {}
|
|
129
|
+
|
|
130
|
+
registerAgent(input: RegisterAgentInput, options?: OperationOptions): Promise<RegisterAgentResult> {
|
|
131
|
+
return request(this.address, this.scopeToken, 'POST', '/v1/agents', input, options)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
listAgents(options?: OperationOptions): Promise<Agent[]> {
|
|
135
|
+
return request(this.address, this.scopeToken, 'GET', '/v1/agents', undefined, options)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async linkAgents(left: string, right: string, options?: OperationOptions): Promise<void> {
|
|
139
|
+
await request(this.address, this.scopeToken, 'POST', '/v1/links', { left, right }, options)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
listEscalations(options?: OperationOptions): Promise<HumanEscalation[]> {
|
|
143
|
+
return request(this.address, this.scopeToken, 'GET', '/v1/scope/escalations', undefined, options)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
resolveEscalation(id: string, answer: string, options?: OperationOptions): Promise<HumanEscalation> {
|
|
147
|
+
return request(this.address, this.scopeToken, 'POST', `/v1/scope/escalations/${encodeURIComponent(id)}/resolve`, {
|
|
148
|
+
answer
|
|
149
|
+
}, options)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export class AutumnBusClient {
|
|
154
|
+
constructor(
|
|
155
|
+
readonly address: string,
|
|
156
|
+
readonly agentToken: string
|
|
157
|
+
) {}
|
|
158
|
+
|
|
159
|
+
heartbeat(lifecycle: AgentLifecycle, ready = true, leaseMs?: number, options?: OperationOptions): Promise<Agent> {
|
|
160
|
+
return request(this.address, this.agentToken, 'PATCH', '/v1/me/heartbeat', {
|
|
161
|
+
lifecycle,
|
|
162
|
+
ready,
|
|
163
|
+
...(leaseMs === undefined ? {} : { leaseMs })
|
|
164
|
+
}, options)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
listPeers(options?: OperationOptions): Promise<Agent[]> {
|
|
168
|
+
return request(this.address, this.agentToken, 'GET', '/v1/peers', undefined, options)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
sendMessage(input: SendMessageInput, options?: OperationOptions): Promise<DeliveryReceipt> {
|
|
172
|
+
return request(this.address, this.agentToken, 'POST', '/v1/messages', input, options)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
receipt(messageId: string, options?: OperationOptions): Promise<DeliveryReceipt> {
|
|
176
|
+
return request(this.address, this.agentToken, 'GET', `/v1/messages/${encodeURIComponent(messageId)}`, undefined, options)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
reserveInbox(limit = 50, options?: OperationOptions): Promise<InboxReservation | null> {
|
|
180
|
+
return request(this.address, this.agentToken, 'POST', '/v1/inbox/reserve', { limit }, options)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
commitInbox(reservationId: string, options?: OperationOptions): Promise<BusMessage[]> {
|
|
184
|
+
return request(
|
|
185
|
+
this.address,
|
|
186
|
+
this.agentToken,
|
|
187
|
+
'POST',
|
|
188
|
+
`/v1/inbox/${encodeURIComponent(reservationId)}/commit`,
|
|
189
|
+
{},
|
|
190
|
+
options
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async releaseInbox(reservationId: string, options?: OperationOptions): Promise<void> {
|
|
195
|
+
await request(
|
|
196
|
+
this.address,
|
|
197
|
+
this.agentToken,
|
|
198
|
+
'POST',
|
|
199
|
+
`/v1/inbox/${encodeURIComponent(reservationId)}/release`,
|
|
200
|
+
{},
|
|
201
|
+
options
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async pullInbox(limit = 50, options?: OperationOptions): Promise<BusMessage[]> {
|
|
206
|
+
const reservation = await this.reserveInbox(limit, options)
|
|
207
|
+
return reservation ? this.commitInbox(reservation.id, options) : []
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async acknowledgeMessages(messageIds: string[], options?: OperationOptions): Promise<number> {
|
|
211
|
+
const result = await request<{ acknowledged: number }>(
|
|
212
|
+
this.address,
|
|
213
|
+
this.agentToken,
|
|
214
|
+
'POST',
|
|
215
|
+
'/v1/messages/ack',
|
|
216
|
+
{ messageIds },
|
|
217
|
+
options
|
|
218
|
+
)
|
|
219
|
+
return result.acknowledged
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
addTask(description: string, dependencies: string[] = [], options?: OperationOptions): Promise<BusTask> {
|
|
223
|
+
return request(this.address, this.agentToken, 'POST', '/v1/tasks', { description, dependencies }, options)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
listTasks(options?: OperationOptions): Promise<BusTask[]> {
|
|
227
|
+
return request(this.address, this.agentToken, 'GET', '/v1/tasks', undefined, options)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
claimTask(taskId: string, options?: OperationOptions): Promise<BusTask> {
|
|
231
|
+
return request(this.address, this.agentToken, 'POST', `/v1/tasks/${encodeURIComponent(taskId)}/claim`, {}, options)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
releaseTask(taskId: string, options?: OperationOptions): Promise<BusTask> {
|
|
235
|
+
return request(this.address, this.agentToken, 'POST', `/v1/tasks/${encodeURIComponent(taskId)}/release`, {}, options)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
completeTask(taskId: string, note?: string, options?: OperationOptions): Promise<BusTask> {
|
|
239
|
+
return request(this.address, this.agentToken, 'POST', `/v1/tasks/${encodeURIComponent(taskId)}/complete`, {
|
|
240
|
+
...(note === undefined ? {} : { note })
|
|
241
|
+
}, options)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
askHuman(input: AskHumanInput, options?: OperationOptions): Promise<HumanEscalation> {
|
|
245
|
+
return request(this.address, this.agentToken, 'POST', '/v1/escalations', input, options)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
escalation(id: string, options?: OperationOptions): Promise<HumanEscalation> {
|
|
249
|
+
return request(this.address, this.agentToken, 'GET', `/v1/escalations/${encodeURIComponent(id)}`, undefined, options)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
mcpEndpoint(): { url: string; headers: Record<string, string> } {
|
|
253
|
+
return { url: `${this.address}/mcp`, headers: { Authorization: `Bearer ${this.agentToken}` } }
|
|
254
|
+
}
|
|
255
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type BusErrorCode =
|
|
2
|
+
| 'INVALID_ARGUMENT'
|
|
3
|
+
| 'UNAUTHENTICATED'
|
|
4
|
+
| 'PERMISSION_DENIED'
|
|
5
|
+
| 'NOT_FOUND'
|
|
6
|
+
| 'METHOD_NOT_ALLOWED'
|
|
7
|
+
| 'CONFLICT'
|
|
8
|
+
| 'BACKPRESSURE'
|
|
9
|
+
| 'INTERNAL'
|
|
10
|
+
|
|
11
|
+
const STATUS_BY_CODE: Readonly<Record<BusErrorCode, number>> = {
|
|
12
|
+
INVALID_ARGUMENT: 400,
|
|
13
|
+
UNAUTHENTICATED: 401,
|
|
14
|
+
PERMISSION_DENIED: 403,
|
|
15
|
+
NOT_FOUND: 404,
|
|
16
|
+
METHOD_NOT_ALLOWED: 405,
|
|
17
|
+
CONFLICT: 409,
|
|
18
|
+
BACKPRESSURE: 429,
|
|
19
|
+
INTERNAL: 500
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class BusError extends Error {
|
|
23
|
+
readonly status: number
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
readonly code: BusErrorCode,
|
|
27
|
+
message: string,
|
|
28
|
+
readonly details?: Record<string, unknown>
|
|
29
|
+
) {
|
|
30
|
+
super(message)
|
|
31
|
+
this.name = 'BusError'
|
|
32
|
+
this.status = STATUS_BY_CODE[code]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function asBusError(error: unknown): BusError {
|
|
37
|
+
return error instanceof BusError ? error : new BusError('INTERNAL', 'Internal Autumn Bus error')
|
|
38
|
+
}
|
package/src/index.ts
ADDED
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
export const AUTUMN_BUS_PROTOCOL_VERSION = '0.1' as const
|
|
2
|
+
|
|
3
|
+
export type ScopeId = string
|
|
4
|
+
export type AgentId = string
|
|
5
|
+
export type ExecutionId = string
|
|
6
|
+
export type MessageId = string
|
|
7
|
+
export type TaskId = string
|
|
8
|
+
|
|
9
|
+
export type AgentLifecycle = 'starting' | 'ready' | 'working' | 'idle' | 'needs_input' | 'offline'
|
|
10
|
+
export type MessageMode = 'notify' | 'request' | 'response'
|
|
11
|
+
export type DeliveryState = 'queued' | 'reserved' | 'delivered' | 'acknowledged' | 'expired'
|
|
12
|
+
export type TaskStatus = 'open' | 'claimed' | 'done'
|
|
13
|
+
export type EscalationStatus = 'pending' | 'resolved'
|
|
14
|
+
|
|
15
|
+
export interface AgentCapability {
|
|
16
|
+
name: string
|
|
17
|
+
description?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AgentIdentity {
|
|
21
|
+
scopeId: ScopeId
|
|
22
|
+
agentId: AgentId
|
|
23
|
+
executionId: ExecutionId
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Agent {
|
|
27
|
+
id: AgentId
|
|
28
|
+
displayName: string
|
|
29
|
+
capabilities: AgentCapability[]
|
|
30
|
+
lifecycle: AgentLifecycle
|
|
31
|
+
ready: boolean
|
|
32
|
+
reachable: boolean
|
|
33
|
+
executionId: ExecutionId
|
|
34
|
+
registeredAt: string
|
|
35
|
+
updatedAt: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ContextItem {
|
|
39
|
+
kind: 'text' | 'file' | 'url' | 'reference'
|
|
40
|
+
title: string
|
|
41
|
+
text?: string
|
|
42
|
+
uri?: string
|
|
43
|
+
mediaType?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BusMessage {
|
|
47
|
+
id: MessageId
|
|
48
|
+
scopeId: ScopeId
|
|
49
|
+
from: AgentId
|
|
50
|
+
to: AgentId
|
|
51
|
+
mode: MessageMode
|
|
52
|
+
body: string
|
|
53
|
+
context: ContextItem[]
|
|
54
|
+
responseTo?: MessageId
|
|
55
|
+
state: DeliveryState
|
|
56
|
+
createdAt: string
|
|
57
|
+
expiresAt?: string
|
|
58
|
+
deliveredAt?: string
|
|
59
|
+
acknowledgedAt?: string
|
|
60
|
+
repliedAt?: string
|
|
61
|
+
responseMessageId?: MessageId
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface DeliveryReceipt {
|
|
65
|
+
messageId: MessageId
|
|
66
|
+
state: DeliveryState
|
|
67
|
+
acceptedAt: string
|
|
68
|
+
deliveredAt?: string
|
|
69
|
+
acknowledgedAt?: string
|
|
70
|
+
repliedAt?: string
|
|
71
|
+
responseMessageId?: MessageId
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface InboxReservation {
|
|
75
|
+
id: string
|
|
76
|
+
expiresAt: string
|
|
77
|
+
messages: BusMessage[]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface BusTask {
|
|
81
|
+
id: TaskId
|
|
82
|
+
scopeId: ScopeId
|
|
83
|
+
description: string
|
|
84
|
+
createdBy: AgentId
|
|
85
|
+
claimedBy?: AgentId
|
|
86
|
+
status: TaskStatus
|
|
87
|
+
dependencies: TaskId[]
|
|
88
|
+
note?: string
|
|
89
|
+
createdAt: string
|
|
90
|
+
updatedAt: string
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface HumanEscalation {
|
|
94
|
+
id: string
|
|
95
|
+
scopeId: ScopeId
|
|
96
|
+
agentId: AgentId
|
|
97
|
+
question: string
|
|
98
|
+
options: string[]
|
|
99
|
+
status: EscalationStatus
|
|
100
|
+
answer?: string
|
|
101
|
+
createdAt: string
|
|
102
|
+
resolvedAt?: string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface CreateScopeInput {
|
|
106
|
+
id?: ScopeId
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface CreateScopeResult {
|
|
110
|
+
scopeId: ScopeId
|
|
111
|
+
scopeToken: string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface RegisterAgentInput {
|
|
115
|
+
id?: AgentId
|
|
116
|
+
displayName: string
|
|
117
|
+
capabilities?: AgentCapability[]
|
|
118
|
+
connectTo?: AgentId[]
|
|
119
|
+
leaseMs?: number
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface RegisterAgentResult extends AgentIdentity {
|
|
123
|
+
agentToken: string
|
|
124
|
+
leaseExpiresAt: string
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface SendMessageInput {
|
|
128
|
+
to: AgentId
|
|
129
|
+
body: string
|
|
130
|
+
mode?: MessageMode
|
|
131
|
+
responseTo?: MessageId
|
|
132
|
+
idempotencyKey?: string
|
|
133
|
+
expiresInMs?: number
|
|
134
|
+
context?: ContextItem[]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface AddTaskInput {
|
|
138
|
+
description: string
|
|
139
|
+
dependencies?: TaskId[]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface AskHumanInput {
|
|
143
|
+
question: string
|
|
144
|
+
options?: string[]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface BusRunFile {
|
|
148
|
+
protocolVersion: string
|
|
149
|
+
address: string
|
|
150
|
+
pid: number
|
|
151
|
+
startedAt: string
|
|
152
|
+
adminToken: string
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface BusHealth {
|
|
156
|
+
name: 'autumn-bus'
|
|
157
|
+
protocolVersion: string
|
|
158
|
+
runtimeVersion: string
|
|
159
|
+
status: 'ready'
|
|
160
|
+
startedAt: string
|
|
161
|
+
}
|
package/src/session.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { AutumnBusClient, AutumnBusScopeClient } from './client.js'
|
|
2
|
+
import type {
|
|
3
|
+
Agent,
|
|
4
|
+
AgentLifecycle,
|
|
5
|
+
BusMessage,
|
|
6
|
+
BusTask,
|
|
7
|
+
RegisterAgentInput,
|
|
8
|
+
RegisterAgentResult
|
|
9
|
+
} from './protocol.js'
|
|
10
|
+
|
|
11
|
+
export interface AgentSessionOptions {
|
|
12
|
+
address: string
|
|
13
|
+
scopeToken: string
|
|
14
|
+
registration: RegisterAgentInput
|
|
15
|
+
heartbeatIntervalMs?: number
|
|
16
|
+
initialLifecycle?: AgentLifecycle
|
|
17
|
+
initialReady?: boolean
|
|
18
|
+
signal?: AbortSignal
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface InboxPollingOptions {
|
|
22
|
+
limit?: number
|
|
23
|
+
intervalMs?: number
|
|
24
|
+
maxIntervalMs?: number
|
|
25
|
+
backoffFactor?: number
|
|
26
|
+
signal?: AbortSignal
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ClaimedTaskResult<T> {
|
|
30
|
+
task: BusTask
|
|
31
|
+
value: T
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function delay(milliseconds: number, signal?: AbortSignal): Promise<void> {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
if (signal?.aborted) {
|
|
37
|
+
reject(signal.reason ?? new Error('Operation aborted'))
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
const onAbort = () => {
|
|
41
|
+
clearTimeout(timer)
|
|
42
|
+
reject(signal?.reason ?? new Error('Operation aborted'))
|
|
43
|
+
}
|
|
44
|
+
const timer = setTimeout(() => {
|
|
45
|
+
signal?.removeEventListener('abort', onAbort)
|
|
46
|
+
resolve()
|
|
47
|
+
}, milliseconds)
|
|
48
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function requiredEnvironmentValue(
|
|
53
|
+
environment: Readonly<Record<string, string | undefined>>,
|
|
54
|
+
name: string
|
|
55
|
+
): string {
|
|
56
|
+
const value = environment[name]
|
|
57
|
+
if (!value) throw new Error(`${name} is required`)
|
|
58
|
+
return value
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function newIdempotencyKey(): string {
|
|
62
|
+
return crypto.randomUUID()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class AutumnBusAgentSession {
|
|
66
|
+
readonly client: AutumnBusClient
|
|
67
|
+
readonly registration: RegisterAgentResult
|
|
68
|
+
readonly done: Promise<void>
|
|
69
|
+
|
|
70
|
+
private readonly leaseMs: number
|
|
71
|
+
private readonly heartbeatIntervalMs: number
|
|
72
|
+
private lifecycle: AgentLifecycle
|
|
73
|
+
private ready: boolean
|
|
74
|
+
private timer: ReturnType<typeof setTimeout> | undefined
|
|
75
|
+
private pendingHeartbeat: Promise<Agent> | undefined
|
|
76
|
+
private resolveDone!: () => void
|
|
77
|
+
private closed = false
|
|
78
|
+
private sessionError: unknown
|
|
79
|
+
|
|
80
|
+
private constructor(options: AgentSessionOptions, registration: RegisterAgentResult) {
|
|
81
|
+
this.registration = registration
|
|
82
|
+
this.client = new AutumnBusClient(options.address, registration.agentToken)
|
|
83
|
+
this.leaseMs = options.registration.leaseMs || 300_000
|
|
84
|
+
this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? Math.floor(this.leaseMs / 3)
|
|
85
|
+
this.lifecycle = options.initialLifecycle ?? 'starting'
|
|
86
|
+
this.ready = options.initialReady ?? false
|
|
87
|
+
this.done = new Promise((resolve) => {
|
|
88
|
+
this.resolveDone = resolve
|
|
89
|
+
})
|
|
90
|
+
options.signal?.addEventListener('abort', () => void this.close(), { once: true })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static async start(options: AgentSessionOptions): Promise<AutumnBusAgentSession> {
|
|
94
|
+
if (options.signal?.aborted) {
|
|
95
|
+
throw options.signal.reason ?? new Error('Operation aborted')
|
|
96
|
+
}
|
|
97
|
+
const leaseMs = options.registration.leaseMs || 300_000
|
|
98
|
+
const interval = options.heartbeatIntervalMs ?? Math.floor(leaseMs / 3)
|
|
99
|
+
const lifecycle = options.initialLifecycle ?? 'starting'
|
|
100
|
+
const ready = options.initialReady ?? false
|
|
101
|
+
if (interval <= 0 || interval >= leaseMs) {
|
|
102
|
+
throw new Error('heartbeatIntervalMs must be shorter than the execution lease')
|
|
103
|
+
}
|
|
104
|
+
if (lifecycle === 'offline' && ready) {
|
|
105
|
+
throw new Error('offline agents cannot be ready')
|
|
106
|
+
}
|
|
107
|
+
const scope = new AutumnBusScopeClient(options.address, options.scopeToken)
|
|
108
|
+
const registration = await scope.registerAgent({ ...options.registration, leaseMs })
|
|
109
|
+
if (options.signal?.aborted) {
|
|
110
|
+
const client = new AutumnBusClient(options.address, registration.agentToken)
|
|
111
|
+
try {
|
|
112
|
+
await client.heartbeat('offline', false, leaseMs)
|
|
113
|
+
} catch {
|
|
114
|
+
// The lease remains the cleanup fallback if the execution was replaced.
|
|
115
|
+
}
|
|
116
|
+
throw options.signal.reason ?? new Error('Operation aborted')
|
|
117
|
+
}
|
|
118
|
+
const session = new AutumnBusAgentSession(options, registration)
|
|
119
|
+
await session.client.heartbeat(session.lifecycle, session.ready, session.leaseMs)
|
|
120
|
+
session.scheduleHeartbeat()
|
|
121
|
+
return session
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get error(): unknown {
|
|
125
|
+
return this.sessionError
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async setState(lifecycle: AgentLifecycle, ready: boolean): Promise<Agent> {
|
|
129
|
+
if (this.closed) throw new Error('agent session is closed')
|
|
130
|
+
if (lifecycle === 'offline' && ready) throw new Error('offline agents cannot be ready')
|
|
131
|
+
this.lifecycle = lifecycle
|
|
132
|
+
this.ready = ready
|
|
133
|
+
return this.client.heartbeat(lifecycle, ready, this.leaseMs)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async close(): Promise<void> {
|
|
137
|
+
if (this.closed) return
|
|
138
|
+
this.closed = true
|
|
139
|
+
if (this.timer !== undefined) clearTimeout(this.timer)
|
|
140
|
+
try {
|
|
141
|
+
await this.pendingHeartbeat
|
|
142
|
+
await this.client.heartbeat('offline', false, this.leaseMs)
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (this.sessionError === undefined) this.sessionError = error
|
|
145
|
+
} finally {
|
|
146
|
+
this.resolveDone()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private scheduleHeartbeat(): void {
|
|
151
|
+
if (this.closed) return
|
|
152
|
+
this.timer = setTimeout(() => {
|
|
153
|
+
this.pendingHeartbeat = this.client.heartbeat(this.lifecycle, this.ready, this.leaseMs)
|
|
154
|
+
void this.pendingHeartbeat.then(
|
|
155
|
+
() => {
|
|
156
|
+
this.pendingHeartbeat = undefined
|
|
157
|
+
this.scheduleHeartbeat()
|
|
158
|
+
},
|
|
159
|
+
(error: unknown) => {
|
|
160
|
+
this.pendingHeartbeat = undefined
|
|
161
|
+
this.sessionError = error
|
|
162
|
+
this.closed = true
|
|
163
|
+
this.resolveDone()
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
}, this.heartbeatIntervalMs)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function* pollInbox(
|
|
171
|
+
client: AutumnBusClient,
|
|
172
|
+
options: InboxPollingOptions = {}
|
|
173
|
+
): AsyncGenerator<BusMessage[]> {
|
|
174
|
+
const limit = options.limit ?? 50
|
|
175
|
+
const intervalMs = options.intervalMs ?? 1_000
|
|
176
|
+
const maxIntervalMs = options.maxIntervalMs ?? 10_000
|
|
177
|
+
const backoffFactor = options.backoffFactor ?? 1.5
|
|
178
|
+
if (limit < 1 || limit > 100) throw new Error('limit must be between 1 and 100')
|
|
179
|
+
if (intervalMs < 1) throw new Error('intervalMs must be positive')
|
|
180
|
+
if (maxIntervalMs < intervalMs) throw new Error('maxIntervalMs must not be shorter than intervalMs')
|
|
181
|
+
if (backoffFactor < 1 || !Number.isFinite(backoffFactor)) {
|
|
182
|
+
throw new Error('backoffFactor must be a finite number of at least 1')
|
|
183
|
+
}
|
|
184
|
+
let nextIntervalMs = intervalMs
|
|
185
|
+
while (!options.signal?.aborted) {
|
|
186
|
+
const messages = await client.pullInbox(limit, options.signal ? { signal: options.signal } : undefined)
|
|
187
|
+
if (messages.length > 0) {
|
|
188
|
+
nextIntervalMs = intervalMs
|
|
189
|
+
yield messages
|
|
190
|
+
} else {
|
|
191
|
+
nextIntervalMs = Math.min(maxIntervalMs, Math.ceil(nextIntervalMs * backoffFactor))
|
|
192
|
+
}
|
|
193
|
+
if (!options.signal?.aborted) await delay(nextIntervalMs, options.signal)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function withClaimedTask<T>(
|
|
198
|
+
client: AutumnBusClient,
|
|
199
|
+
taskId: string,
|
|
200
|
+
work: (task: BusTask) => Promise<T>,
|
|
201
|
+
completionNote?: (value: T) => string | undefined
|
|
202
|
+
): Promise<ClaimedTaskResult<T>> {
|
|
203
|
+
const claimed = await client.claimTask(taskId)
|
|
204
|
+
try {
|
|
205
|
+
const value = await work(claimed)
|
|
206
|
+
const task = await client.completeTask(taskId, completionNote?.(value))
|
|
207
|
+
return { task, value }
|
|
208
|
+
} catch (error) {
|
|
209
|
+
try {
|
|
210
|
+
await client.releaseTask(taskId)
|
|
211
|
+
} catch {
|
|
212
|
+
// Preserve the work error. Lease recovery remains the final fallback.
|
|
213
|
+
}
|
|
214
|
+
throw error
|
|
215
|
+
}
|
|
216
|
+
}
|