@cerebro-labs/agentnava-sdk 0.26.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/LICENSE +202 -0
- package/NOTICE +12 -0
- package/README.md +230 -0
- package/agent.d.ts +285 -0
- package/agent.js +394 -0
- package/agent.js.map +1 -0
- package/attach.d.ts +49 -0
- package/attach.js +115 -0
- package/attach.js.map +1 -0
- package/connection-status.d.ts +28 -0
- package/connection-status.js +61 -0
- package/connection-status.js.map +1 -0
- package/connection.d.ts +89 -0
- package/connection.js +143 -0
- package/connection.js.map +1 -0
- package/conversation.d.ts +177 -0
- package/conversation.js +348 -0
- package/conversation.js.map +1 -0
- package/end-user.d.ts +159 -0
- package/end-user.js +209 -0
- package/end-user.js.map +1 -0
- package/errors.d.ts +43 -0
- package/errors.js +55 -0
- package/errors.js.map +1 -0
- package/http.d.ts +32 -0
- package/http.js +192 -0
- package/http.js.map +1 -0
- package/import-tools.d.ts +282 -0
- package/import-tools.js +686 -0
- package/import-tools.js.map +1 -0
- package/index.d.ts +16 -0
- package/index.js +38 -0
- package/index.js.map +1 -0
- package/knowledge-base.d.ts +165 -0
- package/knowledge-base.js +274 -0
- package/knowledge-base.js.map +1 -0
- package/load-agent-dir.d.ts +17 -0
- package/load-agent-dir.js +192 -0
- package/load-agent-dir.js.map +1 -0
- package/package.json +32 -0
- package/project.d.ts +94 -0
- package/project.js +155 -0
- package/project.js.map +1 -0
- package/secret.d.ts +45 -0
- package/secret.js +69 -0
- package/secret.js.map +1 -0
- package/templates-inline.d.ts +2 -0
- package/templates-inline.js +16 -0
- package/templates-inline.js.map +1 -0
- package/templates.d.ts +16 -0
- package/templates.js +32 -0
- package/templates.js.map +1 -0
- package/turn.d.ts +11 -0
- package/turn.js +244 -0
- package/turn.js.map +1 -0
- package/types.d.ts +485 -0
- package/types.js +4 -0
- package/types.js.map +1 -0
- package/version.d.ts +1 -0
- package/version.js +5 -0
- package/version.js.map +1 -0
- package/workflows.d.ts +36 -0
- package/workflows.js +101 -0
- package/workflows.js.map +1 -0
- package/workspace.d.ts +290 -0
- package/workspace.js +390 -0
- package/workspace.js.map +1 -0
package/turn.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// One turn: `Promise<string> & AsyncIterable<TurnEvent>`.
|
|
2
|
+
//
|
|
3
|
+
// A caller who wants the answer writes `await conversation.ask('...')`. A caller
|
|
4
|
+
// who wants to render it as it arrives writes `for await (const e of
|
|
5
|
+
// conversation.ask('...'))`. One method, two shapes, because making the caller
|
|
6
|
+
// choose `ask` or `askStream` up front means changing the call site later just
|
|
7
|
+
// to show a spinner.
|
|
8
|
+
//
|
|
9
|
+
// THE MESSAGE IS SENT WHEN ask() IS CALLED, not when the result is awaited or
|
|
10
|
+
// iterated. A lazy promise would mean `const t = ask(...)` did nothing and
|
|
11
|
+
// `await t` twice asked twice; sending eagerly makes `ask` the verb it reads as,
|
|
12
|
+
// and calling it twice is then obviously two questions.
|
|
13
|
+
//
|
|
14
|
+
// Awaiting AND iterating the same turn throws rather than replaying. The events
|
|
15
|
+
// are a live stream over a socket, not a stored list, so a second consumer would
|
|
16
|
+
// silently receive nothing at all, which is far worse than an error.
|
|
17
|
+
import { AgentNavaError } from "./errors.js";
|
|
18
|
+
/** Parse an SSE body into the events the API sends.
|
|
19
|
+
*
|
|
20
|
+
* Frames are `data: {json}` separated by blank lines. Anything else on the wire
|
|
21
|
+
* (comments, retry directives, a stray blank) is ignored rather than treated as
|
|
22
|
+
* an error: a proxy is allowed to insert keep-alive comments and a stream that
|
|
23
|
+
* died because of one would be maddening to debug. */
|
|
24
|
+
export async function* parseSSE(body) {
|
|
25
|
+
const reader = body.getReader();
|
|
26
|
+
const decoder = new TextDecoder();
|
|
27
|
+
let buffer = '';
|
|
28
|
+
try {
|
|
29
|
+
while (true) {
|
|
30
|
+
const { done, value } = await reader.read();
|
|
31
|
+
if (done)
|
|
32
|
+
break;
|
|
33
|
+
buffer += decoder.decode(value, { stream: true });
|
|
34
|
+
// Split on newlines, keeping the last partial line in the buffer. A
|
|
35
|
+
// frame can be split across two network reads at any byte.
|
|
36
|
+
let nl = buffer.indexOf('\n');
|
|
37
|
+
while (nl !== -1) {
|
|
38
|
+
const line = buffer.slice(0, nl).replace(/\r$/, '');
|
|
39
|
+
buffer = buffer.slice(nl + 1);
|
|
40
|
+
const event = parseLine(line);
|
|
41
|
+
if (event)
|
|
42
|
+
yield event;
|
|
43
|
+
nl = buffer.indexOf('\n');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// A final frame with no trailing newline still counts.
|
|
47
|
+
const last = parseLine(buffer.replace(/\r$/, ''));
|
|
48
|
+
if (last)
|
|
49
|
+
yield last;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
// Releasing matters on an early break out of a for-await: without it the
|
|
53
|
+
// socket stays open until GC.
|
|
54
|
+
reader.releaseLock();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The API's event vocabulary, as the SDK's.
|
|
59
|
+
*
|
|
60
|
+
* The wire says `message-delta` / `message-end` / `tool-start` / `tool-end`, and
|
|
61
|
+
* design/sdk-design.md says `text` / `done` / `tool`. Both are right for their
|
|
62
|
+
* own audience: the wire shape is what apps/web's chat renders and has rendered
|
|
63
|
+
* for a long time, and the SDK shape is what reads well at a call site
|
|
64
|
+
* (`if (e.type === 'text')`).
|
|
65
|
+
*
|
|
66
|
+
* So this translates, which is the same job the SDK already does for drive
|
|
67
|
+
* entries and connection rows. It is NOT a rename waiting to happen: changing
|
|
68
|
+
* the wire would break a working chat UI to save a switch statement.
|
|
69
|
+
*
|
|
70
|
+
* Returns undefined for a frame with nothing to report, including an event type
|
|
71
|
+
* this version does not know. A new event must never break an old client.
|
|
72
|
+
*/
|
|
73
|
+
function translate(wire) {
|
|
74
|
+
switch (wire.type) {
|
|
75
|
+
case 'message-delta':
|
|
76
|
+
// A FRAGMENT. Append, do not assemble.
|
|
77
|
+
return typeof wire.delta === 'string' && wire.delta.length > 0
|
|
78
|
+
? { type: 'text', text: wire.delta }
|
|
79
|
+
: undefined;
|
|
80
|
+
case 'message-end':
|
|
81
|
+
// Carries the whole reply, which is what an awaiting caller wants.
|
|
82
|
+
return { type: 'done', reply: typeof wire.final === 'string' ? wire.final : '' };
|
|
83
|
+
case 'tool-start':
|
|
84
|
+
return { type: 'tool', name: wire.name ?? 'tool', state: 'start' };
|
|
85
|
+
case 'tool-end':
|
|
86
|
+
return {
|
|
87
|
+
type: 'tool',
|
|
88
|
+
name: wire.name ?? 'tool',
|
|
89
|
+
state: 'end',
|
|
90
|
+
...(wire.ok !== undefined && { ok: wire.ok }),
|
|
91
|
+
};
|
|
92
|
+
case 'error':
|
|
93
|
+
return {
|
|
94
|
+
type: 'error',
|
|
95
|
+
message: wire.message ?? 'the turn failed',
|
|
96
|
+
retryable: wire.retryable === true,
|
|
97
|
+
};
|
|
98
|
+
case 'done':
|
|
99
|
+
// The stream is over. `message-end` already carried the reply, so a
|
|
100
|
+
// second `done` here would be a duplicate terminal event; the caller
|
|
101
|
+
// sees the stream end instead.
|
|
102
|
+
return undefined;
|
|
103
|
+
default:
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function parseLine(line) {
|
|
108
|
+
if (!line.startsWith('data:'))
|
|
109
|
+
return undefined;
|
|
110
|
+
const payload = line.slice(5).trim();
|
|
111
|
+
if (!payload || payload === '[DONE]')
|
|
112
|
+
return undefined;
|
|
113
|
+
try {
|
|
114
|
+
return translate(JSON.parse(payload));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// A frame we cannot parse is dropped rather than thrown. A malformed frame
|
|
118
|
+
// must not break a stream that is otherwise fine.
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Build the dual-shaped result.
|
|
124
|
+
*
|
|
125
|
+
* `response` is already in flight when this is called: the caller passes the
|
|
126
|
+
* running request, so the turn is sent whether or not anyone ever consumes it.
|
|
127
|
+
*
|
|
128
|
+
* A THENABLE, not a Promise subclass, and that was measured rather than assumed.
|
|
129
|
+
* A subclass whose base promise never settles deadlocks, because `await` can
|
|
130
|
+
* settle against the base's own state instead of the overridden `then`. A
|
|
131
|
+
* subclass whose base resolves immediately fixes the deadlock and passes
|
|
132
|
+
* `instanceof Promise`, but then anything reading the promise's INTERNAL state
|
|
133
|
+
* sees "resolved" for a turn whose `then` rejects, which is a lie that tooling
|
|
134
|
+
* believes. A thenable is honest: `await`, `Promise.all` and `.then/.catch/
|
|
135
|
+
* .finally` are all defined in terms of `then` and all work, and anything that
|
|
136
|
+
* demands a real Promise says so out loud instead of reporting a wrong answer.
|
|
137
|
+
*/
|
|
138
|
+
class TurnImpl {
|
|
139
|
+
response;
|
|
140
|
+
consumed;
|
|
141
|
+
settled;
|
|
142
|
+
constructor(response) {
|
|
143
|
+
this.response = response;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Record how this turn is being consumed, and refuse a second, different way.
|
|
147
|
+
*
|
|
148
|
+
* AWAITING TWICE IS ALLOWED, deliberately. A promise handing the same value to
|
|
149
|
+
* two awaits is exactly what a promise is, and `drain()` memoises so the second
|
|
150
|
+
* await costs nothing. Refusing it would break `Promise.all([turn, turn])` and
|
|
151
|
+
* every library that attaches a `.catch()` and then awaits the same value.
|
|
152
|
+
*
|
|
153
|
+
* ITERATING TWICE IS NOT, and neither is mixing the two. The events are a live
|
|
154
|
+
* stream over a socket, not a stored list: a second iterator would receive
|
|
155
|
+
* nothing at all and report an empty turn, which is worse than an error.
|
|
156
|
+
*/
|
|
157
|
+
claim(how) {
|
|
158
|
+
if (how === 'awaited' && this.consumed === 'awaited')
|
|
159
|
+
return;
|
|
160
|
+
if (this.consumed) {
|
|
161
|
+
throw new AgentNavaError({
|
|
162
|
+
message: this.consumed === how
|
|
163
|
+
? `This turn was already ${how}. Call ask() again to ask again; a turn is a live stream, not a replayable list.`
|
|
164
|
+
: `This turn was already ${this.consumed} and cannot also be ${how}. Pick one: await it for the reply, or iterate it for the events. Call ask() again to ask again.`,
|
|
165
|
+
code: 'turn_already_consumed',
|
|
166
|
+
status: 400,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
this.consumed = how;
|
|
170
|
+
}
|
|
171
|
+
async body() {
|
|
172
|
+
const res = await this.response;
|
|
173
|
+
if (!res.body) {
|
|
174
|
+
throw new AgentNavaError({
|
|
175
|
+
message: 'The turn response had no body to stream.',
|
|
176
|
+
code: 'bad_response',
|
|
177
|
+
status: res.status,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return res.body;
|
|
181
|
+
}
|
|
182
|
+
/** Drain the stream and resolve to the final reply. */
|
|
183
|
+
async settle() {
|
|
184
|
+
// `text` fragments accumulate as a fallback. `done` carries the whole reply
|
|
185
|
+
// and wins when present, but a stream that ends without one should return
|
|
186
|
+
// what the caller actually saw rather than an empty string, which would read
|
|
187
|
+
// as "the agent said nothing".
|
|
188
|
+
let assembled = '';
|
|
189
|
+
for await (const event of parseSSE(await this.body())) {
|
|
190
|
+
if (event.type === 'text')
|
|
191
|
+
assembled += event.text;
|
|
192
|
+
if (event.type === 'done')
|
|
193
|
+
return event.reply;
|
|
194
|
+
if (event.type === 'error') {
|
|
195
|
+
// Awaiting is the shape that throws. Iterating gets the error as an
|
|
196
|
+
// event and the loop ends, because a for-await that throws mid-render
|
|
197
|
+
// leaves the UI half-painted.
|
|
198
|
+
throw new AgentNavaError({
|
|
199
|
+
message: event.message,
|
|
200
|
+
code: 'turn_failed',
|
|
201
|
+
status: event.retryable ? 503 : 400,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return assembled;
|
|
206
|
+
}
|
|
207
|
+
drain() {
|
|
208
|
+
// claim() runs HERE rather than inside settle(), because settle() runs only
|
|
209
|
+
// once: a second await would skip the check entirely, and awaiting a turn
|
|
210
|
+
// that was already iterated would quietly return an empty string instead of
|
|
211
|
+
// saying what went wrong.
|
|
212
|
+
//
|
|
213
|
+
// REJECT, never throw. `then` must return a promise; a synchronous throw out
|
|
214
|
+
// of it escapes the promise chain, so a caller writing
|
|
215
|
+
// `turn.then(a, b).catch(c)` would get an exception past their own .catch.
|
|
216
|
+
try {
|
|
217
|
+
this.claim('awaited');
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
return Promise.reject(err);
|
|
221
|
+
}
|
|
222
|
+
// Started once. `await` calls `then`, and so does Promise.all, and so does
|
|
223
|
+
// anything that duck-types a thenable; all of them share one drain.
|
|
224
|
+
this.settled ??= this.settle();
|
|
225
|
+
return this.settled;
|
|
226
|
+
}
|
|
227
|
+
then(onOk, onErr) {
|
|
228
|
+
return this.drain().then(onOk, onErr);
|
|
229
|
+
}
|
|
230
|
+
catch(onErr) {
|
|
231
|
+
return this.drain().catch(onErr);
|
|
232
|
+
}
|
|
233
|
+
finally(onDone) {
|
|
234
|
+
return this.drain().finally(onDone);
|
|
235
|
+
}
|
|
236
|
+
async *[Symbol.asyncIterator]() {
|
|
237
|
+
this.claim('iterated');
|
|
238
|
+
yield* parseSSE(await this.body());
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
export function makeTurn(response) {
|
|
242
|
+
return new TurnImpl(response);
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=turn.js.map
|
package/turn.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn.js","sourceRoot":"","sources":["../src/turn.ts"],"names":[],"mappings":"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","sourcesContent":["// One turn: `Promise<string> & AsyncIterable<TurnEvent>`.\n//\n// A caller who wants the answer writes `await conversation.ask('...')`. A caller\n// who wants to render it as it arrives writes `for await (const e of\n// conversation.ask('...'))`. One method, two shapes, because making the caller\n// choose `ask` or `askStream` up front means changing the call site later just\n// to show a spinner.\n//\n// THE MESSAGE IS SENT WHEN ask() IS CALLED, not when the result is awaited or\n// iterated. A lazy promise would mean `const t = ask(...)` did nothing and\n// `await t` twice asked twice; sending eagerly makes `ask` the verb it reads as,\n// and calling it twice is then obviously two questions.\n//\n// Awaiting AND iterating the same turn throws rather than replaying. The events\n// are a live stream over a socket, not a stored list, so a second consumer would\n// silently receive nothing at all, which is far worse than an error.\n\nimport { AgentNavaError } from './errors.ts';\nimport type { TurnEvent } from './types.ts';\n\n/** What `conversation.ask()` returns. */\nexport type Turn = Promise<string> & AsyncIterable<TurnEvent>;\n\n/** Parse an SSE body into the events the API sends.\n *\n * Frames are `data: {json}` separated by blank lines. Anything else on the wire\n * (comments, retry directives, a stray blank) is ignored rather than treated as\n * an error: a proxy is allowed to insert keep-alive comments and a stream that\n * died because of one would be maddening to debug. */\nexport async function* parseSSE(body: ReadableStream<Uint8Array>): AsyncGenerator<TurnEvent> {\n\tconst reader = body.getReader();\n\tconst decoder = new TextDecoder();\n\tlet buffer = '';\n\ttry {\n\t\twhile (true) {\n\t\t\tconst { done, value } = await reader.read();\n\t\t\tif (done) break;\n\t\t\tbuffer += decoder.decode(value, { stream: true });\n\t\t\t// Split on newlines, keeping the last partial line in the buffer. A\n\t\t\t// frame can be split across two network reads at any byte.\n\t\t\tlet nl = buffer.indexOf('\\n');\n\t\t\twhile (nl !== -1) {\n\t\t\t\tconst line = buffer.slice(0, nl).replace(/\\r$/, '');\n\t\t\t\tbuffer = buffer.slice(nl + 1);\n\t\t\t\tconst event = parseLine(line);\n\t\t\t\tif (event) yield event;\n\t\t\t\tnl = buffer.indexOf('\\n');\n\t\t\t}\n\t\t}\n\t\t// A final frame with no trailing newline still counts.\n\t\tconst last = parseLine(buffer.replace(/\\r$/, ''));\n\t\tif (last) yield last;\n\t} finally {\n\t\t// Releasing matters on an early break out of a for-await: without it the\n\t\t// socket stays open until GC.\n\t\treader.releaseLock();\n\t}\n}\n\n/** One frame as it arrives on the wire, before translation. */\ninterface WireEvent {\n\ttype: string;\n\tdelta?: string;\n\tfinal?: string;\n\tname?: string;\n\tok?: boolean;\n\tmessage?: string;\n\tretryable?: boolean;\n\tpath?: string;\n}\n\n/**\n * The API's event vocabulary, as the SDK's.\n *\n * The wire says `message-delta` / `message-end` / `tool-start` / `tool-end`, and\n * design/sdk-design.md says `text` / `done` / `tool`. Both are right for their\n * own audience: the wire shape is what apps/web's chat renders and has rendered\n * for a long time, and the SDK shape is what reads well at a call site\n * (`if (e.type === 'text')`).\n *\n * So this translates, which is the same job the SDK already does for drive\n * entries and connection rows. It is NOT a rename waiting to happen: changing\n * the wire would break a working chat UI to save a switch statement.\n *\n * Returns undefined for a frame with nothing to report, including an event type\n * this version does not know. A new event must never break an old client.\n */\nfunction translate(wire: WireEvent): TurnEvent | undefined {\n\tswitch (wire.type) {\n\t\tcase 'message-delta':\n\t\t\t// A FRAGMENT. Append, do not assemble.\n\t\t\treturn typeof wire.delta === 'string' && wire.delta.length > 0\n\t\t\t\t? { type: 'text', text: wire.delta }\n\t\t\t\t: undefined;\n\t\tcase 'message-end':\n\t\t\t// Carries the whole reply, which is what an awaiting caller wants.\n\t\t\treturn { type: 'done', reply: typeof wire.final === 'string' ? wire.final : '' };\n\t\tcase 'tool-start':\n\t\t\treturn { type: 'tool', name: wire.name ?? 'tool', state: 'start' };\n\t\tcase 'tool-end':\n\t\t\treturn {\n\t\t\t\ttype: 'tool',\n\t\t\t\tname: wire.name ?? 'tool',\n\t\t\t\tstate: 'end',\n\t\t\t\t...(wire.ok !== undefined && { ok: wire.ok }),\n\t\t\t};\n\t\tcase 'error':\n\t\t\treturn {\n\t\t\t\ttype: 'error',\n\t\t\t\tmessage: wire.message ?? 'the turn failed',\n\t\t\t\tretryable: wire.retryable === true,\n\t\t\t};\n\t\tcase 'done':\n\t\t\t// The stream is over. `message-end` already carried the reply, so a\n\t\t\t// second `done` here would be a duplicate terminal event; the caller\n\t\t\t// sees the stream end instead.\n\t\t\treturn undefined;\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\nfunction parseLine(line: string): TurnEvent | undefined {\n\tif (!line.startsWith('data:')) return undefined;\n\tconst payload = line.slice(5).trim();\n\tif (!payload || payload === '[DONE]') return undefined;\n\ttry {\n\t\treturn translate(JSON.parse(payload) as WireEvent);\n\t} catch {\n\t\t// A frame we cannot parse is dropped rather than thrown. A malformed frame\n\t\t// must not break a stream that is otherwise fine.\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Build the dual-shaped result.\n *\n * `response` is already in flight when this is called: the caller passes the\n * running request, so the turn is sent whether or not anyone ever consumes it.\n *\n * A THENABLE, not a Promise subclass, and that was measured rather than assumed.\n * A subclass whose base promise never settles deadlocks, because `await` can\n * settle against the base's own state instead of the overridden `then`. A\n * subclass whose base resolves immediately fixes the deadlock and passes\n * `instanceof Promise`, but then anything reading the promise's INTERNAL state\n * sees \"resolved\" for a turn whose `then` rejects, which is a lie that tooling\n * believes. A thenable is honest: `await`, `Promise.all` and `.then/.catch/\n * .finally` are all defined in terms of `then` and all work, and anything that\n * demands a real Promise says so out loud instead of reporting a wrong answer.\n */\nclass TurnImpl implements AsyncIterable<TurnEvent> {\n\tprivate consumed?: 'awaited' | 'iterated';\n\tprivate settled?: Promise<string>;\n\n\tconstructor(private readonly response: Promise<Response>) {}\n\n\t/**\n\t * Record how this turn is being consumed, and refuse a second, different way.\n\t *\n\t * AWAITING TWICE IS ALLOWED, deliberately. A promise handing the same value to\n\t * two awaits is exactly what a promise is, and `drain()` memoises so the second\n\t * await costs nothing. Refusing it would break `Promise.all([turn, turn])` and\n\t * every library that attaches a `.catch()` and then awaits the same value.\n\t *\n\t * ITERATING TWICE IS NOT, and neither is mixing the two. The events are a live\n\t * stream over a socket, not a stored list: a second iterator would receive\n\t * nothing at all and report an empty turn, which is worse than an error.\n\t */\n\tprivate claim(how: 'awaited' | 'iterated'): void {\n\t\tif (how === 'awaited' && this.consumed === 'awaited') return;\n\t\tif (this.consumed) {\n\t\t\tthrow new AgentNavaError({\n\t\t\t\tmessage:\n\t\t\t\t\tthis.consumed === how\n\t\t\t\t\t\t? `This turn was already ${how}. Call ask() again to ask again; a turn is a live stream, not a replayable list.`\n\t\t\t\t\t\t: `This turn was already ${this.consumed} and cannot also be ${how}. Pick one: await it for the reply, or iterate it for the events. Call ask() again to ask again.`,\n\t\t\t\tcode: 'turn_already_consumed',\n\t\t\t\tstatus: 400,\n\t\t\t});\n\t\t}\n\t\tthis.consumed = how;\n\t}\n\n\tprivate async body(): Promise<ReadableStream<Uint8Array>> {\n\t\tconst res = await this.response;\n\t\tif (!res.body) {\n\t\t\tthrow new AgentNavaError({\n\t\t\t\tmessage: 'The turn response had no body to stream.',\n\t\t\t\tcode: 'bad_response',\n\t\t\t\tstatus: res.status,\n\t\t\t});\n\t\t}\n\t\treturn res.body;\n\t}\n\n\t/** Drain the stream and resolve to the final reply. */\n\tprivate async settle(): Promise<string> {\n\t\t// `text` fragments accumulate as a fallback. `done` carries the whole reply\n\t\t// and wins when present, but a stream that ends without one should return\n\t\t// what the caller actually saw rather than an empty string, which would read\n\t\t// as \"the agent said nothing\".\n\t\tlet assembled = '';\n\t\tfor await (const event of parseSSE(await this.body())) {\n\t\t\tif (event.type === 'text') assembled += event.text;\n\t\t\tif (event.type === 'done') return event.reply;\n\t\t\tif (event.type === 'error') {\n\t\t\t\t// Awaiting is the shape that throws. Iterating gets the error as an\n\t\t\t\t// event and the loop ends, because a for-await that throws mid-render\n\t\t\t\t// leaves the UI half-painted.\n\t\t\t\tthrow new AgentNavaError({\n\t\t\t\t\tmessage: event.message,\n\t\t\t\t\tcode: 'turn_failed',\n\t\t\t\t\tstatus: event.retryable ? 503 : 400,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\treturn assembled;\n\t}\n\n\tprivate drain(): Promise<string> {\n\t\t// claim() runs HERE rather than inside settle(), because settle() runs only\n\t\t// once: a second await would skip the check entirely, and awaiting a turn\n\t\t// that was already iterated would quietly return an empty string instead of\n\t\t// saying what went wrong.\n\t\t//\n\t\t// REJECT, never throw. `then` must return a promise; a synchronous throw out\n\t\t// of it escapes the promise chain, so a caller writing\n\t\t// `turn.then(a, b).catch(c)` would get an exception past their own .catch.\n\t\ttry {\n\t\t\tthis.claim('awaited');\n\t\t} catch (err) {\n\t\t\treturn Promise.reject(err);\n\t\t}\n\t\t// Started once. `await` calls `then`, and so does Promise.all, and so does\n\t\t// anything that duck-types a thenable; all of them share one drain.\n\t\tthis.settled ??= this.settle();\n\t\treturn this.settled;\n\t}\n\n\tthen<A = string, B = never>(\n\t\tonOk?: ((value: string) => A | PromiseLike<A>) | null,\n\t\tonErr?: ((reason: unknown) => B | PromiseLike<B>) | null,\n\t): Promise<A | B> {\n\t\treturn this.drain().then(onOk, onErr);\n\t}\n\n\tcatch<B = never>(onErr?: ((reason: unknown) => B | PromiseLike<B>) | null): Promise<string | B> {\n\t\treturn this.drain().catch(onErr);\n\t}\n\n\tfinally(onDone?: (() => void) | null): Promise<string> {\n\t\treturn this.drain().finally(onDone);\n\t}\n\n\tasync *[Symbol.asyncIterator](): AsyncGenerator<TurnEvent> {\n\t\tthis.claim('iterated');\n\t\tyield* parseSSE(await this.body());\n\t}\n}\n\nexport function makeTurn(response: Promise<Response>): Turn {\n\treturn new TurnImpl(response) as unknown as Turn;\n}\n"]}
|