@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/transport.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport seam for the Agent App SDK. The client speaks only these two verbs - a request/response
|
|
3
|
+
* `command` and a Server-Sent-Events `stream` - so a fake, a replay driver, and a real loopback RPC
|
|
4
|
+
* host are interchangeable behind one interface.
|
|
5
|
+
*
|
|
6
|
+
* Caller credentials are never stored on the transport. The optional {@link AuthProvider} is invoked
|
|
7
|
+
* per request to mint a bearer token; the token is placed on the outgoing `Authorization` header and
|
|
8
|
+
* is never copied into a result, an error, or a log line.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { AgentEvent } from "@nifrajs/agent-protocol"
|
|
12
|
+
import { isAgentEvent } from "@nifrajs/agent-protocol"
|
|
13
|
+
|
|
14
|
+
/** One RPC-style call. `params` is JSON-serializable; `signal` cancels the in-flight request. */
|
|
15
|
+
export interface AgentTransportRequest {
|
|
16
|
+
readonly method: string
|
|
17
|
+
readonly params?: unknown
|
|
18
|
+
readonly signal?: AbortSignal
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The outcome of a command. Non-ok responses never throw here so a caller can render a bounded state
|
|
23
|
+
* instead of unwinding; `error` carries only the server's status text, never a credential.
|
|
24
|
+
*/
|
|
25
|
+
export type CommandOutcome<T> =
|
|
26
|
+
| { readonly ok: true; readonly status: number; readonly value: T }
|
|
27
|
+
| { readonly ok: false; readonly status: number; readonly error: string }
|
|
28
|
+
|
|
29
|
+
export interface AgentTransport {
|
|
30
|
+
command<T = unknown>(request: AgentTransportRequest): Promise<CommandOutcome<T>>
|
|
31
|
+
stream(request: AgentTransportRequest): AsyncIterable<AgentEvent>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Returns a bearer token for the next request, or `undefined` for an unauthenticated call. */
|
|
35
|
+
export type AuthProvider = () => string | undefined | Promise<string | undefined>
|
|
36
|
+
|
|
37
|
+
export interface HttpAgentTransportOptions {
|
|
38
|
+
/** RPC origin, e.g. `http://127.0.0.1:8787`. The `/rpc` path is appended. */
|
|
39
|
+
readonly endpoint: string
|
|
40
|
+
/** Per-request bearer-token source. Never stored; only the header value is used and discarded. */
|
|
41
|
+
readonly authorize?: AuthProvider
|
|
42
|
+
/** Injectable fetch for tests and non-DOM hosts. Defaults to the ambient `fetch`. */
|
|
43
|
+
readonly fetch?: typeof fetch
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Thrown only for transport-level faults (network, malformed body). Carries no credential. */
|
|
47
|
+
export class AgentTransportError extends Error {
|
|
48
|
+
constructor(
|
|
49
|
+
readonly method: string,
|
|
50
|
+
reason: string,
|
|
51
|
+
) {
|
|
52
|
+
super(`agent transport: ${method}: ${reason}`)
|
|
53
|
+
this.name = "AgentTransportError"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Web `fetch` + SSE transport. Browser- and Bun-compatible; depends on no Node or framework code. */
|
|
58
|
+
export class HttpAgentTransport implements AgentTransport {
|
|
59
|
+
private readonly url: string
|
|
60
|
+
private readonly authorize: AuthProvider | undefined
|
|
61
|
+
private readonly fetchImpl: typeof fetch
|
|
62
|
+
|
|
63
|
+
constructor(options: HttpAgentTransportOptions) {
|
|
64
|
+
if (typeof options.endpoint !== "string" || options.endpoint.length === 0)
|
|
65
|
+
throw new TypeError("agent transport: endpoint is required")
|
|
66
|
+
this.url = `${options.endpoint.replace(/\/$/, "")}/rpc`
|
|
67
|
+
this.authorize = options.authorize
|
|
68
|
+
const bound = options.fetch ?? globalThis.fetch
|
|
69
|
+
if (typeof bound !== "function") throw new TypeError("agent transport: no fetch available")
|
|
70
|
+
this.fetchImpl = bound.bind(globalThis)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private async headers(accept: string): Promise<Headers> {
|
|
74
|
+
const headers = new Headers({ "content-type": "application/json", accept })
|
|
75
|
+
const token = await this.authorize?.()
|
|
76
|
+
if (token !== undefined && token.length > 0) headers.set("authorization", `Bearer ${token}`)
|
|
77
|
+
return headers
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async command<T = unknown>(request: AgentTransportRequest): Promise<CommandOutcome<T>> {
|
|
81
|
+
const body = serialize(request)
|
|
82
|
+
let response: Response
|
|
83
|
+
try {
|
|
84
|
+
response = await this.fetchImpl(this.url, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: await this.headers("application/json"),
|
|
87
|
+
body,
|
|
88
|
+
...(request.signal ? { signal: request.signal } : {}),
|
|
89
|
+
})
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw new AgentTransportError(request.method, describe(error))
|
|
92
|
+
}
|
|
93
|
+
const text = await response.text()
|
|
94
|
+
if (!response.ok)
|
|
95
|
+
return { ok: false, status: response.status, error: text || response.statusText }
|
|
96
|
+
if (text.length === 0) return { ok: true, status: response.status, value: undefined as T }
|
|
97
|
+
try {
|
|
98
|
+
return { ok: true, status: response.status, value: JSON.parse(text) as T }
|
|
99
|
+
} catch {
|
|
100
|
+
throw new AgentTransportError(request.method, "response body was not JSON")
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async *stream(request: AgentTransportRequest): AsyncIterable<AgentEvent> {
|
|
105
|
+
let response: Response
|
|
106
|
+
try {
|
|
107
|
+
response = await this.fetchImpl(this.url, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: await this.headers("text/event-stream"),
|
|
110
|
+
body: serialize(request),
|
|
111
|
+
...(request.signal ? { signal: request.signal } : {}),
|
|
112
|
+
})
|
|
113
|
+
} catch (error) {
|
|
114
|
+
throw new AgentTransportError(request.method, describe(error))
|
|
115
|
+
}
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
const text = await response.text()
|
|
118
|
+
throw new AgentTransportError(request.method, text || `status ${response.status}`)
|
|
119
|
+
}
|
|
120
|
+
if (response.body === null) return
|
|
121
|
+
yield* parseEventStream(response.body, request.method)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Parse an SSE body into protocol events, skipping any frame whose data is not a valid event. */
|
|
126
|
+
export async function* parseEventStream(
|
|
127
|
+
body: ReadableStream<Uint8Array>,
|
|
128
|
+
method: string,
|
|
129
|
+
): AsyncIterable<AgentEvent> {
|
|
130
|
+
const reader = body.getReader()
|
|
131
|
+
const decoder = new TextDecoder()
|
|
132
|
+
let buffer = ""
|
|
133
|
+
try {
|
|
134
|
+
for (;;) {
|
|
135
|
+
const chunk = await reader.read()
|
|
136
|
+
if (chunk.done) break
|
|
137
|
+
buffer += decoder.decode(chunk.value, { stream: true })
|
|
138
|
+
let boundary = buffer.indexOf("\n\n")
|
|
139
|
+
while (boundary !== -1) {
|
|
140
|
+
const frame = buffer.slice(0, boundary)
|
|
141
|
+
buffer = buffer.slice(boundary + 2)
|
|
142
|
+
const event = frameToEvent(frame)
|
|
143
|
+
if (event !== undefined) yield event
|
|
144
|
+
boundary = buffer.indexOf("\n\n")
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw new AgentTransportError(method, describe(error))
|
|
149
|
+
} finally {
|
|
150
|
+
reader.releaseLock()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function frameToEvent(frame: string): AgentEvent | undefined {
|
|
155
|
+
const data = frame
|
|
156
|
+
.split("\n")
|
|
157
|
+
.filter((line) => line.startsWith("data:"))
|
|
158
|
+
.map((line) => line.slice(5).replace(/^ /, ""))
|
|
159
|
+
.join("\n")
|
|
160
|
+
if (data.length === 0) return undefined
|
|
161
|
+
let value: unknown
|
|
162
|
+
try {
|
|
163
|
+
value = JSON.parse(data)
|
|
164
|
+
} catch {
|
|
165
|
+
return undefined
|
|
166
|
+
}
|
|
167
|
+
return isAgentEvent(value) ? value : undefined
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function serialize(request: AgentTransportRequest): string {
|
|
171
|
+
return JSON.stringify({
|
|
172
|
+
method: request.method,
|
|
173
|
+
...(request.params === undefined ? {} : { params: request.params }),
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function describe(error: unknown): string {
|
|
178
|
+
return error instanceof Error ? error.message : String(error)
|
|
179
|
+
}
|