@nifrajs/agent-protocol 3.2.0

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/src/index.ts ADDED
@@ -0,0 +1,405 @@
1
+ /**
2
+ * Backend-neutral protocol for local coding-agent hosts.
3
+ *
4
+ * This package deliberately has no runtime dependencies. A Pi adapter, a Nifra-native backend,
5
+ * the CLI, and the Workbench can all implement or consume these contracts without pulling any
6
+ * framework, model provider, or UI code into an application.
7
+ */
8
+
9
+ export const AGENT_PROTOCOL_VERSION = 1 as const
10
+
11
+ export type AgentSessionStatus = "idle" | "running" | "waiting" | "completed" | "failed" | "stopped"
12
+
13
+ export type AgentEventType =
14
+ | "session.started"
15
+ | "session.updated"
16
+ | "turn.started"
17
+ | "assistant.delta"
18
+ | "assistant.message"
19
+ | "tool.started"
20
+ | "tool.delta"
21
+ | "tool.completed"
22
+ | "approval.required"
23
+ | "approval.resolved"
24
+ | "repair.required"
25
+ | "verification.completed"
26
+ | "memory.compacted"
27
+ | "extension.reloaded"
28
+ | "session.completed"
29
+ | "session.failed"
30
+ | "session.stopped"
31
+
32
+ export interface AgentSessionSnapshot {
33
+ readonly version: typeof AGENT_PROTOCOL_VERSION
34
+ readonly id: string
35
+ readonly backend: string
36
+ readonly cwd: string
37
+ readonly status: AgentSessionStatus
38
+ readonly createdAt: number
39
+ readonly updatedAt: number
40
+ readonly lastSeq: number
41
+ readonly activeTurnId?: string
42
+ readonly extensionRevision?: string
43
+ readonly capabilities: readonly string[]
44
+ }
45
+
46
+ export interface AgentBackendInfo {
47
+ readonly name: string
48
+ readonly version?: string
49
+ readonly capabilities: readonly string[]
50
+ }
51
+
52
+ export interface AgentSessionEventBase {
53
+ readonly version: typeof AGENT_PROTOCOL_VERSION
54
+ readonly sessionId: string
55
+ readonly seq: number
56
+ readonly at: number
57
+ readonly type: AgentEventType
58
+ }
59
+
60
+ export interface AgentSessionStartedEvent extends AgentSessionEventBase {
61
+ readonly type: "session.started"
62
+ readonly snapshot: AgentSessionSnapshot
63
+ }
64
+
65
+ export interface AgentSessionUpdatedEvent extends AgentSessionEventBase {
66
+ readonly type: "session.updated"
67
+ readonly snapshot: AgentSessionSnapshot
68
+ }
69
+
70
+ export interface AgentTurnStartedEvent extends AgentSessionEventBase {
71
+ readonly type: "turn.started"
72
+ readonly turnId: string
73
+ readonly prompt: string
74
+ }
75
+
76
+ export interface AgentAssistantDeltaEvent extends AgentSessionEventBase {
77
+ readonly type: "assistant.delta"
78
+ readonly turnId: string
79
+ readonly text: string
80
+ }
81
+
82
+ export interface AgentAssistantMessageEvent extends AgentSessionEventBase {
83
+ readonly type: "assistant.message"
84
+ readonly turnId: string
85
+ readonly text: string
86
+ }
87
+
88
+ export interface AgentToolStartedEvent extends AgentSessionEventBase {
89
+ readonly type: "tool.started"
90
+ readonly turnId: string
91
+ readonly callId: string
92
+ readonly name: string
93
+ readonly input?: unknown
94
+ }
95
+
96
+ export interface AgentToolDeltaEvent extends AgentSessionEventBase {
97
+ readonly type: "tool.delta"
98
+ readonly turnId: string
99
+ readonly callId: string
100
+ readonly text: string
101
+ }
102
+
103
+ export interface AgentToolCompletedEvent extends AgentSessionEventBase {
104
+ readonly type: "tool.completed"
105
+ readonly turnId: string
106
+ readonly callId: string
107
+ readonly name: string
108
+ readonly ok: boolean
109
+ readonly output?: unknown
110
+ readonly error?: AgentError
111
+ }
112
+
113
+ export interface AgentApprovalRequiredEvent extends AgentSessionEventBase {
114
+ readonly type: "approval.required"
115
+ readonly turnId: string
116
+ readonly approvalId: string
117
+ readonly action: string
118
+ readonly capability: string
119
+ readonly reason?: string
120
+ }
121
+
122
+ export interface AgentApprovalResolvedEvent extends AgentSessionEventBase {
123
+ readonly type: "approval.resolved"
124
+ readonly turnId?: string
125
+ readonly approvalId: string
126
+ readonly approved: boolean
127
+ readonly reason?: string
128
+ }
129
+
130
+ export interface AgentRepairTask {
131
+ readonly id: string
132
+ readonly verification: "check" | "assure" | "test"
133
+ readonly cwd: string
134
+ readonly reason: string
135
+ readonly capabilities: readonly string[]
136
+ readonly output?: string
137
+ readonly report?: unknown
138
+ }
139
+
140
+ export interface AgentRepairRequiredEvent extends AgentSessionEventBase {
141
+ readonly type: "repair.required"
142
+ readonly turnId?: string
143
+ readonly task: AgentRepairTask
144
+ }
145
+
146
+ export interface AgentVerificationCompletedEvent extends AgentSessionEventBase {
147
+ readonly type: "verification.completed"
148
+ readonly name: string
149
+ readonly ok: boolean
150
+ readonly report?: unknown
151
+ }
152
+
153
+ export interface AgentMemoryCompactedEvent extends AgentSessionEventBase {
154
+ readonly type: "memory.compacted"
155
+ readonly before: number
156
+ readonly after: number
157
+ readonly reason: "manual" | "threshold" | "overflow" | "workflow"
158
+ }
159
+
160
+ export interface AgentExtensionReloadedEvent extends AgentSessionEventBase {
161
+ readonly type: "extension.reloaded"
162
+ readonly revision: string
163
+ readonly loaded: readonly string[]
164
+ readonly disabled: readonly string[]
165
+ readonly rolledBack: boolean
166
+ }
167
+
168
+ export interface AgentSessionCompletedEvent extends AgentSessionEventBase {
169
+ readonly type: "session.completed"
170
+ readonly snapshot: AgentSessionSnapshot
171
+ }
172
+
173
+ export interface AgentSessionFailedEvent extends AgentSessionEventBase {
174
+ readonly type: "session.failed"
175
+ readonly error: AgentError
176
+ readonly recoverable: boolean
177
+ }
178
+
179
+ export interface AgentSessionStoppedEvent extends AgentSessionEventBase {
180
+ readonly type: "session.stopped"
181
+ readonly reason?: string
182
+ }
183
+
184
+ export type AgentEvent =
185
+ | AgentSessionStartedEvent
186
+ | AgentSessionUpdatedEvent
187
+ | AgentTurnStartedEvent
188
+ | AgentAssistantDeltaEvent
189
+ | AgentAssistantMessageEvent
190
+ | AgentToolStartedEvent
191
+ | AgentToolDeltaEvent
192
+ | AgentToolCompletedEvent
193
+ | AgentApprovalRequiredEvent
194
+ | AgentApprovalResolvedEvent
195
+ | AgentRepairRequiredEvent
196
+ | AgentVerificationCompletedEvent
197
+ | AgentMemoryCompactedEvent
198
+ | AgentExtensionReloadedEvent
199
+ | AgentSessionCompletedEvent
200
+ | AgentSessionFailedEvent
201
+ | AgentSessionStoppedEvent
202
+
203
+ /** Event payload before the transport adds protocol version, session id, sequence, and timestamp. */
204
+ export type AgentEventPayload =
205
+ | Omit<AgentSessionStartedEvent, "version" | "sessionId" | "seq" | "at">
206
+ | Omit<AgentSessionUpdatedEvent, "version" | "sessionId" | "seq" | "at">
207
+ | Omit<AgentTurnStartedEvent, "version" | "sessionId" | "seq" | "at">
208
+ | Omit<AgentAssistantDeltaEvent, "version" | "sessionId" | "seq" | "at">
209
+ | Omit<AgentAssistantMessageEvent, "version" | "sessionId" | "seq" | "at">
210
+ | Omit<AgentToolStartedEvent, "version" | "sessionId" | "seq" | "at">
211
+ | Omit<AgentToolDeltaEvent, "version" | "sessionId" | "seq" | "at">
212
+ | Omit<AgentToolCompletedEvent, "version" | "sessionId" | "seq" | "at">
213
+ | Omit<AgentApprovalRequiredEvent, "version" | "sessionId" | "seq" | "at">
214
+ | Omit<AgentApprovalResolvedEvent, "version" | "sessionId" | "seq" | "at">
215
+ | Omit<AgentRepairRequiredEvent, "version" | "sessionId" | "seq" | "at">
216
+ | Omit<AgentVerificationCompletedEvent, "version" | "sessionId" | "seq" | "at">
217
+ | Omit<AgentMemoryCompactedEvent, "version" | "sessionId" | "seq" | "at">
218
+ | Omit<AgentExtensionReloadedEvent, "version" | "sessionId" | "seq" | "at">
219
+ | Omit<AgentSessionCompletedEvent, "version" | "sessionId" | "seq" | "at">
220
+ | Omit<AgentSessionFailedEvent, "version" | "sessionId" | "seq" | "at">
221
+ | Omit<AgentSessionStoppedEvent, "version" | "sessionId" | "seq" | "at">
222
+
223
+ export interface AgentError {
224
+ readonly code: string
225
+ readonly message: string
226
+ readonly details?: unknown
227
+ }
228
+
229
+ export interface CreateSessionInput {
230
+ readonly cwd: string
231
+ readonly backend?: string
232
+ readonly capabilities?: readonly string[]
233
+ readonly sessionId?: string
234
+ }
235
+
236
+ export interface SendMessageInput {
237
+ readonly sessionId: string
238
+ readonly message: string
239
+ readonly signal?: AbortSignal
240
+ }
241
+
242
+ export interface ForkSessionInput {
243
+ readonly sessionId: string
244
+ readonly targetSessionId?: string
245
+ }
246
+
247
+ export interface ForkSessionResult {
248
+ readonly sessionId: string
249
+ readonly parentSessionId: string
250
+ }
251
+
252
+ export interface AgentSessionCheckpoint {
253
+ readonly version: 1
254
+ readonly sessionId: string
255
+ readonly at: number
256
+ readonly payload?: unknown
257
+ }
258
+
259
+ export interface ReloadResult {
260
+ readonly revision: string
261
+ readonly loaded: readonly string[]
262
+ readonly disabled: readonly string[]
263
+ readonly rolledBack: boolean
264
+ readonly error?: AgentError
265
+ }
266
+
267
+ export interface AgentBackend {
268
+ readonly info: AgentBackendInfo
269
+ createSession(input: CreateSessionInput): Promise<AgentSessionSnapshot>
270
+ send(input: SendMessageInput): AsyncIterable<AgentEvent>
271
+ cancel(sessionId: string, reason?: string): Promise<void>
272
+ snapshot(sessionId: string): Promise<AgentSessionSnapshot>
273
+ /** Backends may provide native branching; the host can fall back to its session store. */
274
+ fork?(input: ForkSessionInput): Promise<ForkSessionResult>
275
+ checkpoint?(sessionId: string, payload?: unknown): Promise<AgentSessionCheckpoint>
276
+ reload(sessionId: string): Promise<ReloadResult>
277
+ /** Resolve an approval surfaced by a backend UI/tool, when that backend supports interactive approvals. */
278
+ resolveApproval?(
279
+ sessionId: string,
280
+ approvalId: string,
281
+ approved: boolean,
282
+ reason?: string,
283
+ ): Promise<boolean | undefined>
284
+ close(sessionId: string): Promise<void>
285
+ }
286
+
287
+ export interface AgentEventSink {
288
+ push(event: AgentEvent): void
289
+ complete(): void
290
+ fail(error: unknown): void
291
+ }
292
+
293
+ export interface AgentEventStream extends AgentEventSink, AsyncIterableIterator<AgentEvent> {
294
+ readonly dropped: number
295
+ }
296
+
297
+ /**
298
+ * Small bounded event stream for RPC clients and UIs. The authoritative event history belongs to
299
+ * the backend/session store; this live view may drop old transient events if a consumer falls behind.
300
+ */
301
+ export function createAgentEventStream(maxQueueSize = 256): AgentEventStream {
302
+ if (!Number.isSafeInteger(maxQueueSize) || maxQueueSize < 1)
303
+ throw new RangeError("agent event stream: maxQueueSize must be a positive safe integer")
304
+
305
+ const queue: AgentEvent[] = []
306
+ const waiters: Array<{
307
+ readonly resolve: (result: IteratorResult<AgentEvent>) => void
308
+ readonly reject: (reason?: unknown) => void
309
+ }> = []
310
+ let completed = false
311
+ let failure: unknown
312
+ let failed = false
313
+ let dropped = 0
314
+
315
+ const finish = (): void => {
316
+ if (!completed || queue.length > 0) return
317
+ while (waiters.length > 0) {
318
+ const waiter = waiters.shift()
319
+ if (failed) waiter?.reject(failure)
320
+ else waiter?.resolve({ done: true, value: undefined })
321
+ }
322
+ }
323
+
324
+ const stream: AgentEventStream = {
325
+ get dropped() {
326
+ return dropped
327
+ },
328
+ push(event) {
329
+ if (completed) return
330
+ const waiter = waiters.shift()
331
+ if (waiter !== undefined) {
332
+ waiter.resolve({ done: false, value: event })
333
+ return
334
+ }
335
+ if (queue.length >= maxQueueSize) {
336
+ queue.shift()
337
+ dropped += 1
338
+ }
339
+ queue.push(event)
340
+ },
341
+ complete() {
342
+ completed = true
343
+ finish()
344
+ },
345
+ fail(error) {
346
+ if (completed) return
347
+ failure = error
348
+ failed = true
349
+ completed = true
350
+ finish()
351
+ },
352
+ next() {
353
+ const event = queue.shift()
354
+ if (event !== undefined) return Promise.resolve({ done: false, value: event })
355
+ if (completed) {
356
+ if (failed) return Promise.reject(failure)
357
+ return Promise.resolve({ done: true, value: undefined })
358
+ }
359
+ return new Promise<IteratorResult<AgentEvent>>((resolve, reject) =>
360
+ waiters.push({ resolve, reject }),
361
+ )
362
+ },
363
+ return() {
364
+ queue.length = 0
365
+ completed = true
366
+ finish()
367
+ return Promise.resolve({ done: true, value: undefined })
368
+ },
369
+ [Symbol.asyncIterator]() {
370
+ return this
371
+ },
372
+ }
373
+ return stream
374
+ }
375
+
376
+ export function agentError(code: string, message: string, details?: unknown): AgentError {
377
+ return details === undefined ? { code, message } : { code, message, details }
378
+ }
379
+
380
+ /**
381
+ * Orchestration value contracts (run plans, content-free evidence, artifact refs, effect keys).
382
+ * Additive to the session protocol; carries no behavior and no payload content.
383
+ */
384
+ export * from "./orchestration.ts"
385
+
386
+ /**
387
+ * Additive run-lifecycle contracts (run snapshots, evidence-event envelopes, handoff snapshots,
388
+ * feature negotiation, cursor resume). Additive to the session protocol; carries no payload content.
389
+ */
390
+ export * from "./run-lifecycle.ts"
391
+
392
+ export function isAgentEvent(value: unknown): value is AgentEvent {
393
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false
394
+ const record = value as Record<string, unknown>
395
+ return (
396
+ record.version === AGENT_PROTOCOL_VERSION &&
397
+ typeof record.sessionId === "string" &&
398
+ typeof record.seq === "number" &&
399
+ Number.isSafeInteger(record.seq) &&
400
+ typeof record.at === "number" &&
401
+ Number.isFinite(record.at) &&
402
+ typeof record.type === "string" &&
403
+ record.type.includes(".")
404
+ )
405
+ }