@nifrajs/agent-app 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/README.md +70 -0
- package/dist/client.d.ts +150 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +340 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/transport.d.ts +61 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +145 -0
- package/dist/transport.js.map +1 -0
- package/dist/view-models.d.ts +283 -0
- package/dist/view-models.d.ts.map +1 -0
- package/dist/view-models.js +579 -0
- package/dist/view-models.js.map +1 -0
- package/package.json +49 -0
- package/src/client.ts +465 -0
- package/src/index.ts +74 -0
- package/src/transport.ts +179 -0
- package/src/view-models.ts +919 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Agent App client: a presentation-safe façade over an agent host.
|
|
3
|
+
*
|
|
4
|
+
* It negotiates the feature set with the host on session creation, drives turns as an ordered,
|
|
5
|
+
* deduplicated stream of content-free {@link AgentEventView}s, resumes a persisted log by cursor with
|
|
6
|
+
* gap detection, and resolves approvals and handoffs. Everything it returns upward is a view model -
|
|
7
|
+
* identifiers, statuses, counters, opaque references - so an untyped browser app built on this client
|
|
8
|
+
* can never touch prompt text, tool payloads, model output, or filesystem paths. Host-specific surfaces
|
|
9
|
+
* beyond the negotiated contract are reachable through the raw {@link AgentAppClient.command} escape
|
|
10
|
+
* hatch, which still returns a bounded {@link CommandOutcome} and never leaks the caller credential.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
type AgentSessionSnapshot,
|
|
15
|
+
type BoundaryCommand,
|
|
16
|
+
type DecisionCoordinate,
|
|
17
|
+
negotiateFeatures,
|
|
18
|
+
parseHandoffSnapshot,
|
|
19
|
+
parseRunSnapshot,
|
|
20
|
+
} from "@nifrajs/agent-protocol"
|
|
21
|
+
import type { AgentTransport, CommandOutcome } from "./transport.ts"
|
|
22
|
+
import {
|
|
23
|
+
type AgentEventView,
|
|
24
|
+
type HandoffView,
|
|
25
|
+
OrderedEventBuffer,
|
|
26
|
+
type RunView,
|
|
27
|
+
type SessionView,
|
|
28
|
+
toEventView,
|
|
29
|
+
toHandoffView,
|
|
30
|
+
toRunView,
|
|
31
|
+
toSessionView,
|
|
32
|
+
} from "./view-models.ts"
|
|
33
|
+
|
|
34
|
+
/** Interaction features the client knows how to drive. A host grants the subset it supports. */
|
|
35
|
+
export const AGENT_APP_FEATURES = [
|
|
36
|
+
"approvals",
|
|
37
|
+
"checkpoint",
|
|
38
|
+
"fork",
|
|
39
|
+
"handoff",
|
|
40
|
+
"inbox",
|
|
41
|
+
"reload",
|
|
42
|
+
"resume",
|
|
43
|
+
"workflows",
|
|
44
|
+
] as const
|
|
45
|
+
|
|
46
|
+
export class AgentAppError extends Error {
|
|
47
|
+
constructor(message: string) {
|
|
48
|
+
super(message)
|
|
49
|
+
this.name = "AgentAppError"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface AgentAppClientOptions {
|
|
54
|
+
/** Features the client will request at negotiation. Defaults to {@link AGENT_APP_FEATURES}. */
|
|
55
|
+
readonly features?: readonly string[]
|
|
56
|
+
/** Bound on out-of-order live events held before a gap is skipped. Forwarded to the event buffer. */
|
|
57
|
+
readonly maxPending?: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface CreateSessionInput {
|
|
61
|
+
readonly cwd?: string
|
|
62
|
+
readonly backend?: string
|
|
63
|
+
readonly capabilities?: readonly string[]
|
|
64
|
+
readonly sessionId?: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ResumeInput {
|
|
68
|
+
/** Last delivered seq. Omit or pass `-1` to replay the whole retained window. */
|
|
69
|
+
readonly cursor?: number
|
|
70
|
+
readonly limit?: number
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** One persisted log record reduced to ordering and a type label - never its payload. */
|
|
74
|
+
export interface ReplayEntryView {
|
|
75
|
+
readonly seq: number
|
|
76
|
+
readonly at: number
|
|
77
|
+
readonly type: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type ReplayResult =
|
|
81
|
+
| {
|
|
82
|
+
readonly status: "ok"
|
|
83
|
+
readonly nextCursor: number
|
|
84
|
+
readonly entries: readonly ReplayEntryView[]
|
|
85
|
+
readonly session?: SessionView
|
|
86
|
+
}
|
|
87
|
+
| {
|
|
88
|
+
readonly status: "resync_required"
|
|
89
|
+
readonly reason: string
|
|
90
|
+
readonly earliest: number
|
|
91
|
+
readonly latest: number
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface PendingApprovalView {
|
|
95
|
+
readonly approvalId: string
|
|
96
|
+
readonly action: string
|
|
97
|
+
readonly capability: string
|
|
98
|
+
readonly turnId?: string
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface ResolveHandoffInput {
|
|
102
|
+
readonly runId: string
|
|
103
|
+
readonly nodeId: string
|
|
104
|
+
readonly accept: boolean
|
|
105
|
+
readonly reason?: string
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* One pending decision boundary in the inbox, projected to structural coordinates and a state only.
|
|
110
|
+
* It never carries a prompt, a free-text reason, tool data, model output, a diagnostic, or an
|
|
111
|
+
* artifact - just enough identity for the UI to address the exact boundary a decision resolves.
|
|
112
|
+
*/
|
|
113
|
+
export interface BoundaryItemView {
|
|
114
|
+
readonly kind: "approval" | "handoff"
|
|
115
|
+
readonly requestId: string
|
|
116
|
+
readonly runId: string
|
|
117
|
+
readonly nodeId: string
|
|
118
|
+
readonly capability: string
|
|
119
|
+
readonly vector: number
|
|
120
|
+
readonly expiresAt: number
|
|
121
|
+
readonly state: string
|
|
122
|
+
/** Opaque owner role once a handoff is assigned. */
|
|
123
|
+
readonly to?: string
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The bounded outcome of a boundary decision: the resulting item, or a content-free refusal code. */
|
|
127
|
+
export type BoundaryDecisionResult =
|
|
128
|
+
| { readonly ok: true; readonly item: BoundaryItemView }
|
|
129
|
+
| { readonly ok: false; readonly code: string }
|
|
130
|
+
|
|
131
|
+
export class AgentAppClient {
|
|
132
|
+
private readonly transport: AgentTransport
|
|
133
|
+
private readonly requested: readonly string[]
|
|
134
|
+
private readonly maxPending: number | undefined
|
|
135
|
+
private currentSession: SessionView | undefined
|
|
136
|
+
private granted = new Set<string>()
|
|
137
|
+
|
|
138
|
+
constructor(transport: AgentTransport, options?: AgentAppClientOptions) {
|
|
139
|
+
this.transport = transport
|
|
140
|
+
this.requested = options?.features ?? AGENT_APP_FEATURES
|
|
141
|
+
this.maxPending = options?.maxPending
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The current session view, or `undefined` before {@link createSession}/{@link resume}. */
|
|
145
|
+
get session(): SessionView | undefined {
|
|
146
|
+
return this.currentSession
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Features the host granted at negotiation. */
|
|
150
|
+
get features(): readonly string[] {
|
|
151
|
+
return [...this.granted]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
supports(feature: string): boolean {
|
|
155
|
+
return this.granted.has(feature)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
requireFeature(feature: string): void {
|
|
159
|
+
if (!this.granted.has(feature))
|
|
160
|
+
throw new AgentAppError(`host did not grant the "${feature}" feature`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Open a session and negotiate the feature set against the host's advertised capabilities. */
|
|
164
|
+
async createSession(input: CreateSessionInput = {}): Promise<SessionView> {
|
|
165
|
+
const outcome = await this.transport.command<unknown>({
|
|
166
|
+
method: "session.create",
|
|
167
|
+
params: input,
|
|
168
|
+
})
|
|
169
|
+
const snapshot = this.expectSnapshot(outcome, "session.create")
|
|
170
|
+
this.adoptSnapshot(snapshot)
|
|
171
|
+
return this.currentSession as SessionView
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Stream a turn as ordered, deduplicated view models. The underlying transport receives the host's
|
|
176
|
+
* full event stream, but every value yielded here is content-free.
|
|
177
|
+
*/
|
|
178
|
+
async *send(message: string, options?: { signal?: AbortSignal }): AsyncIterable<AgentEventView> {
|
|
179
|
+
if (this.currentSession === undefined)
|
|
180
|
+
throw new AgentAppError("createSession must be called before send")
|
|
181
|
+
const buffer = new OrderedEventBuffer({
|
|
182
|
+
from: this.currentSession.lastSeq,
|
|
183
|
+
...(this.maxPending === undefined ? {} : { maxPending: this.maxPending }),
|
|
184
|
+
})
|
|
185
|
+
const stream = this.transport.stream({
|
|
186
|
+
method: "turn.send",
|
|
187
|
+
params: { message },
|
|
188
|
+
...(options?.signal ? { signal: options.signal } : {}),
|
|
189
|
+
})
|
|
190
|
+
for await (const event of stream) {
|
|
191
|
+
for (const view of buffer.offer(toEventView(event))) yield view
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Resume a persisted log by cursor. A cursor whose next record was evicted asks for a resync. */
|
|
196
|
+
async resume(input: ResumeInput = {}): Promise<ReplayResult> {
|
|
197
|
+
this.requireFeature("resume")
|
|
198
|
+
const params: Record<string, number> = { cursor: input.cursor ?? -1 }
|
|
199
|
+
if (input.limit !== undefined) params.limit = input.limit
|
|
200
|
+
const outcome = await this.transport.command<unknown>({ method: "session.events", params })
|
|
201
|
+
if (!outcome.ok) throw commandError("session.events", outcome)
|
|
202
|
+
return this.decodeResume(outcome.value)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** List approvals awaiting a decision, projected to their identifiers only. */
|
|
206
|
+
async listApprovals(): Promise<readonly PendingApprovalView[]> {
|
|
207
|
+
this.requireFeature("approvals")
|
|
208
|
+
const outcome = await this.transport.command<unknown>({ method: "approval.list" })
|
|
209
|
+
if (!outcome.ok) throw commandError("approval.list", outcome)
|
|
210
|
+
const pending = isRecord(outcome.value) ? outcome.value.pending : undefined
|
|
211
|
+
if (!Array.isArray(pending)) return []
|
|
212
|
+
const views: PendingApprovalView[] = []
|
|
213
|
+
for (const item of pending) {
|
|
214
|
+
if (!isRecord(item)) continue
|
|
215
|
+
const { approvalId, action, capability, turnId } = item
|
|
216
|
+
if (
|
|
217
|
+
typeof approvalId !== "string" ||
|
|
218
|
+
typeof action !== "string" ||
|
|
219
|
+
typeof capability !== "string"
|
|
220
|
+
)
|
|
221
|
+
continue
|
|
222
|
+
views.push(
|
|
223
|
+
Object.freeze({
|
|
224
|
+
approvalId,
|
|
225
|
+
action,
|
|
226
|
+
capability,
|
|
227
|
+
...(typeof turnId === "string" ? { turnId } : {}),
|
|
228
|
+
}),
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
return views
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Approve or decline a pending approval. Returns the host's recorded decision. */
|
|
235
|
+
async resolveApproval(approvalId: string, approved: boolean, reason?: string): Promise<boolean> {
|
|
236
|
+
this.requireFeature("approvals")
|
|
237
|
+
const outcome = await this.transport.command<unknown>({
|
|
238
|
+
method: "approval.resolve",
|
|
239
|
+
params: { approvalId, approved, ...(reason === undefined ? {} : { reason }) },
|
|
240
|
+
})
|
|
241
|
+
if (!outcome.ok) throw commandError("approval.resolve", outcome)
|
|
242
|
+
return isRecord(outcome.value) && outcome.value.approved === true
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Accept or decline a handoff. Returns the resulting handoff view when the host reports one. */
|
|
246
|
+
async resolveHandoff(input: ResolveHandoffInput): Promise<HandoffView | undefined> {
|
|
247
|
+
this.requireFeature("handoff")
|
|
248
|
+
const outcome = await this.transport.command<unknown>({
|
|
249
|
+
method: "handoff.resolve",
|
|
250
|
+
params: {
|
|
251
|
+
runId: input.runId,
|
|
252
|
+
nodeId: input.nodeId,
|
|
253
|
+
accept: input.accept,
|
|
254
|
+
...(input.reason === undefined ? {} : { reason: input.reason }),
|
|
255
|
+
},
|
|
256
|
+
})
|
|
257
|
+
if (!outcome.ok) throw commandError("handoff.resolve", outcome)
|
|
258
|
+
if (!isRecord(outcome.value) || outcome.value.handoff === undefined) return undefined
|
|
259
|
+
return toHandoffView(parseHandoffSnapshot(outcome.value.handoff))
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* List every pending decision boundary (approvals and handoffs) as content-free item views. Gated
|
|
264
|
+
* on the negotiated `inbox` feature.
|
|
265
|
+
*/
|
|
266
|
+
async listBoundaries(): Promise<readonly BoundaryItemView[]> {
|
|
267
|
+
this.requireFeature("inbox")
|
|
268
|
+
const outcome = await this.transport.command<unknown>({ method: "boundary.list" })
|
|
269
|
+
if (!outcome.ok) throw commandError("boundary.list", outcome)
|
|
270
|
+
const items = isRecord(outcome.value) ? outcome.value.items : undefined
|
|
271
|
+
if (!Array.isArray(items)) return []
|
|
272
|
+
const views: BoundaryItemView[] = []
|
|
273
|
+
for (const item of items) {
|
|
274
|
+
const view = toBoundaryItemView(item)
|
|
275
|
+
if (view !== undefined) views.push(view)
|
|
276
|
+
}
|
|
277
|
+
return Object.freeze(views)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Inspect one boundary by request id, projected to a content-free item view. */
|
|
281
|
+
async inspectBoundary(requestId: string): Promise<BoundaryItemView | undefined> {
|
|
282
|
+
this.requireFeature("inbox")
|
|
283
|
+
const outcome = await this.transport.command<unknown>({
|
|
284
|
+
method: "boundary.inspect",
|
|
285
|
+
params: { requestId },
|
|
286
|
+
})
|
|
287
|
+
if (!outcome.ok) throw commandError("boundary.inspect", outcome)
|
|
288
|
+
const source =
|
|
289
|
+
isRecord(outcome.value) && outcome.value.item !== undefined
|
|
290
|
+
? outcome.value.item
|
|
291
|
+
: outcome.value
|
|
292
|
+
return toBoundaryItemView(source)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Drive one boundary command against its exact coordinate. Because the full {@link DecisionCoordinate}
|
|
297
|
+
* (run, node, capability, request id, vector, expiry) rides on every call, a decision can only ever
|
|
298
|
+
* resolve the boundary it names - never a different item - and the host fails a stale or mismatched
|
|
299
|
+
* coordinate closed. `assign` carries the owner role in `to`.
|
|
300
|
+
*/
|
|
301
|
+
async decideBoundary(
|
|
302
|
+
command: BoundaryCommand,
|
|
303
|
+
coordinate: DecisionCoordinate,
|
|
304
|
+
options?: { readonly to?: string },
|
|
305
|
+
): Promise<BoundaryDecisionResult> {
|
|
306
|
+
this.requireFeature("inbox")
|
|
307
|
+
const outcome = await this.transport.command<unknown>({
|
|
308
|
+
method: `boundary.${command}`,
|
|
309
|
+
params: { coordinate, ...(options?.to === undefined ? {} : { to: options.to }) },
|
|
310
|
+
})
|
|
311
|
+
if (!outcome.ok) throw commandError(`boundary.${command}`, outcome)
|
|
312
|
+
if (isRecord(outcome.value) && typeof outcome.value.rejected === "string")
|
|
313
|
+
return { ok: false, code: outcome.value.rejected }
|
|
314
|
+
const item = toBoundaryItemView(
|
|
315
|
+
isRecord(outcome.value) && outcome.value.item !== undefined
|
|
316
|
+
? outcome.value.item
|
|
317
|
+
: outcome.value,
|
|
318
|
+
)
|
|
319
|
+
if (item === undefined) return { ok: false, code: "invalid_response" }
|
|
320
|
+
return { ok: true, item }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Fetch a run snapshot by id, projected to a content-free {@link RunView}. */
|
|
324
|
+
async runSnapshot(runId: string): Promise<RunView> {
|
|
325
|
+
const outcome = await this.transport.command<unknown>({
|
|
326
|
+
method: "run.snapshot",
|
|
327
|
+
params: { runId },
|
|
328
|
+
})
|
|
329
|
+
if (!outcome.ok) throw commandError("run.snapshot", outcome)
|
|
330
|
+
const value =
|
|
331
|
+
isRecord(outcome.value) && outcome.value.snapshot !== undefined
|
|
332
|
+
? outcome.value.snapshot
|
|
333
|
+
: outcome.value
|
|
334
|
+
return toRunView(parseRunSnapshot(value))
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Cancel the active turn / stop the session. */
|
|
338
|
+
async cancel(reason?: string): Promise<void> {
|
|
339
|
+
const outcome = await this.transport.command<unknown>({
|
|
340
|
+
method: "session.stop",
|
|
341
|
+
params: reason === undefined ? {} : { reason },
|
|
342
|
+
})
|
|
343
|
+
if (!outcome.ok) throw commandError("session.stop", outcome)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Escape hatch for host-specific commands outside the negotiated contract. Returns a bounded
|
|
348
|
+
* {@link CommandOutcome}; the caller is responsible for keeping whatever it renders content-free.
|
|
349
|
+
*/
|
|
350
|
+
command<T = unknown>(
|
|
351
|
+
method: string,
|
|
352
|
+
params?: unknown,
|
|
353
|
+
signal?: AbortSignal,
|
|
354
|
+
): Promise<CommandOutcome<T>> {
|
|
355
|
+
return this.transport.command<T>({
|
|
356
|
+
method,
|
|
357
|
+
...(params === undefined ? {} : { params }),
|
|
358
|
+
...(signal ? { signal } : {}),
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private adoptSnapshot(snapshot: AgentSessionSnapshot): void {
|
|
363
|
+
this.currentSession = toSessionView(snapshot)
|
|
364
|
+
this.granted = new Set(negotiateFeatures(snapshot.capabilities, this.requested).granted)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private expectSnapshot(outcome: CommandOutcome<unknown>, method: string): AgentSessionSnapshot {
|
|
368
|
+
if (!outcome.ok) throw commandError(method, outcome)
|
|
369
|
+
const value = outcome.value
|
|
370
|
+
if (
|
|
371
|
+
!isRecord(value) ||
|
|
372
|
+
typeof value.id !== "string" ||
|
|
373
|
+
typeof value.backend !== "string" ||
|
|
374
|
+
typeof value.status !== "string" ||
|
|
375
|
+
typeof value.createdAt !== "number" ||
|
|
376
|
+
typeof value.updatedAt !== "number" ||
|
|
377
|
+
typeof value.lastSeq !== "number" ||
|
|
378
|
+
!Array.isArray(value.capabilities)
|
|
379
|
+
)
|
|
380
|
+
throw new AgentAppError(`${method} did not return a session snapshot`)
|
|
381
|
+
return value as unknown as AgentSessionSnapshot
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private decodeResume(value: unknown): ReplayResult {
|
|
385
|
+
if (!isRecord(value)) throw new AgentAppError("session.events returned a non-object")
|
|
386
|
+
const resume = isRecord(value.resume) ? value.resume : value
|
|
387
|
+
const session =
|
|
388
|
+
isRecord(value.snapshot) && typeof value.snapshot.id === "string"
|
|
389
|
+
? toSessionView(value.snapshot as unknown as AgentSessionSnapshot)
|
|
390
|
+
: undefined
|
|
391
|
+
if (resume.status === "resync_required") {
|
|
392
|
+
return Object.freeze({
|
|
393
|
+
status: "resync_required",
|
|
394
|
+
reason: typeof resume.reason === "string" ? resume.reason : "stale_cursor",
|
|
395
|
+
earliest: typeof resume.earliest === "number" ? resume.earliest : 0,
|
|
396
|
+
latest: typeof resume.latest === "number" ? resume.latest : 0,
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
const source = Array.isArray(resume.events)
|
|
400
|
+
? resume.events
|
|
401
|
+
: Array.isArray(value.entries)
|
|
402
|
+
? value.entries
|
|
403
|
+
: []
|
|
404
|
+
const entries: ReplayEntryView[] = []
|
|
405
|
+
for (const item of source) {
|
|
406
|
+
if (!isRecord(item)) continue
|
|
407
|
+
if (
|
|
408
|
+
typeof item.seq !== "number" ||
|
|
409
|
+
typeof item.at !== "number" ||
|
|
410
|
+
typeof item.type !== "string"
|
|
411
|
+
)
|
|
412
|
+
continue
|
|
413
|
+
entries.push(Object.freeze({ seq: item.seq, at: item.at, type: item.type }))
|
|
414
|
+
}
|
|
415
|
+
const tail = entries.at(-1)
|
|
416
|
+
const nextCursor =
|
|
417
|
+
typeof resume.nextCursor === "number"
|
|
418
|
+
? resume.nextCursor
|
|
419
|
+
: typeof resume.cursor === "number"
|
|
420
|
+
? resume.cursor
|
|
421
|
+
: (tail?.seq ?? -1)
|
|
422
|
+
return Object.freeze({
|
|
423
|
+
status: "ok",
|
|
424
|
+
nextCursor,
|
|
425
|
+
entries,
|
|
426
|
+
...(session === undefined ? {} : { session }),
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function commandError(method: string, outcome: { status: number; error: string }): AgentAppError {
|
|
432
|
+
return new AgentAppError(`${method} failed (${outcome.status}): ${outcome.error}`)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
436
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Project a raw boundary record to a content-free {@link BoundaryItemView}, or drop it if malformed. */
|
|
440
|
+
function toBoundaryItemView(value: unknown): BoundaryItemView | undefined {
|
|
441
|
+
if (!isRecord(value)) return undefined
|
|
442
|
+
const { kind, requestId, runId, nodeId, capability, vector, expiresAt, state, to } = value
|
|
443
|
+
if (
|
|
444
|
+
(kind !== "approval" && kind !== "handoff") ||
|
|
445
|
+
typeof requestId !== "string" ||
|
|
446
|
+
typeof runId !== "string" ||
|
|
447
|
+
typeof nodeId !== "string" ||
|
|
448
|
+
typeof capability !== "string" ||
|
|
449
|
+
typeof vector !== "number" ||
|
|
450
|
+
typeof expiresAt !== "number" ||
|
|
451
|
+
typeof state !== "string"
|
|
452
|
+
)
|
|
453
|
+
return undefined
|
|
454
|
+
return Object.freeze({
|
|
455
|
+
kind,
|
|
456
|
+
requestId,
|
|
457
|
+
runId,
|
|
458
|
+
nodeId,
|
|
459
|
+
capability,
|
|
460
|
+
vector,
|
|
461
|
+
expiresAt,
|
|
462
|
+
state,
|
|
463
|
+
...(typeof to === "string" ? { to } : {}),
|
|
464
|
+
})
|
|
465
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nifrajs/agent-app` - a presentation-safe browser SDK for Nifra agent hosts.
|
|
3
|
+
*
|
|
4
|
+
* This package depends only on `@nifrajs/agent-protocol`. It has no backend, provider, storage, model,
|
|
5
|
+
* or UI-framework dependency, so it can be bundled into browser-facing code without dragging a private
|
|
6
|
+
* engine or payload content across the boundary. Everything it surfaces upward is a content-free view
|
|
7
|
+
* model: identifiers, lifecycle statuses, counters, and opaque references.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
AGENT_APP_FEATURES,
|
|
12
|
+
AgentAppClient,
|
|
13
|
+
type AgentAppClientOptions,
|
|
14
|
+
AgentAppError,
|
|
15
|
+
type BoundaryDecisionResult,
|
|
16
|
+
type BoundaryItemView,
|
|
17
|
+
type CreateSessionInput,
|
|
18
|
+
type PendingApprovalView,
|
|
19
|
+
type ReplayEntryView,
|
|
20
|
+
type ReplayResult,
|
|
21
|
+
type ResolveHandoffInput,
|
|
22
|
+
type ResumeInput,
|
|
23
|
+
} from "./client.ts"
|
|
24
|
+
export {
|
|
25
|
+
type AgentTransport,
|
|
26
|
+
AgentTransportError,
|
|
27
|
+
type AgentTransportRequest,
|
|
28
|
+
type AuthProvider,
|
|
29
|
+
type CommandOutcome,
|
|
30
|
+
HttpAgentTransport,
|
|
31
|
+
type HttpAgentTransportOptions,
|
|
32
|
+
parseEventStream,
|
|
33
|
+
} from "./transport.ts"
|
|
34
|
+
export {
|
|
35
|
+
type AgentEventView,
|
|
36
|
+
type ApprovalRequiredView,
|
|
37
|
+
type ApprovalResolvedView,
|
|
38
|
+
type AssistantChunkView,
|
|
39
|
+
type BoundaryStateView,
|
|
40
|
+
boundaryCommands,
|
|
41
|
+
boundaryIsStale,
|
|
42
|
+
type EvalComparisonView,
|
|
43
|
+
type EvidenceTimelineView,
|
|
44
|
+
type ExtensionReloadedView,
|
|
45
|
+
type FaultInjectionView,
|
|
46
|
+
type HandoffView,
|
|
47
|
+
type MemoryCompactedView,
|
|
48
|
+
OrderedEventBuffer,
|
|
49
|
+
type RegistryCapabilityView,
|
|
50
|
+
type RepairRequiredView,
|
|
51
|
+
type RunStudioNodeState,
|
|
52
|
+
type RunStudioNodeView,
|
|
53
|
+
type RunStudioView,
|
|
54
|
+
type RunView,
|
|
55
|
+
type SessionFailedView,
|
|
56
|
+
type SessionLifecycleView,
|
|
57
|
+
type SessionStoppedView,
|
|
58
|
+
type SessionView,
|
|
59
|
+
type ToolCompletedView,
|
|
60
|
+
type ToolDeltaView,
|
|
61
|
+
type ToolStartedView,
|
|
62
|
+
type TurnStartedView,
|
|
63
|
+
toEvalComparisonView,
|
|
64
|
+
toEventView,
|
|
65
|
+
toEvidenceTimelineView,
|
|
66
|
+
toFaultInjectionViews,
|
|
67
|
+
toHandoffView,
|
|
68
|
+
toRegistryCapabilityView,
|
|
69
|
+
toRunStudioView,
|
|
70
|
+
toRunView,
|
|
71
|
+
toSessionView,
|
|
72
|
+
type VerificationCompletedView,
|
|
73
|
+
virtualizeEvidenceRows,
|
|
74
|
+
} from "./view-models.ts"
|