@bunny-agent/sdk 0.9.62 → 0.10.0-beta.1
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 +30 -1
- package/dist/chunk-WQI667GT.js +106 -0
- package/dist/chunk-WQI667GT.js.map +1 -0
- package/dist/contracts.d.ts +25 -0
- package/dist/contracts.js +51 -0
- package/dist/contracts.js.map +1 -0
- package/dist/index.d.ts +54 -4
- package/dist/index.js +517 -59
- package/dist/index.js.map +1 -1
- package/package.json +10 -5
package/dist/index.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isSafeHttpUrl,
|
|
3
|
+
isSearchContractCapabilities,
|
|
4
|
+
isValidCitationContractMetadata
|
|
5
|
+
} from "./chunk-WQI667GT.js";
|
|
6
|
+
|
|
1
7
|
// src/index.ts
|
|
2
8
|
import {
|
|
3
9
|
buildRunnerEnv,
|
|
@@ -40,6 +46,203 @@ function getProviderLogger(settings) {
|
|
|
40
46
|
};
|
|
41
47
|
}
|
|
42
48
|
|
|
49
|
+
// src/provider/search-contract-ai-sdk.ts
|
|
50
|
+
import { createRequire } from "module";
|
|
51
|
+
var SEARCH_CONTRACT_AI_SDK_MINIMUM = "6.0.264";
|
|
52
|
+
function supportsSearchContractUiMetadata(version) {
|
|
53
|
+
const match = /^6\.(\d+)\.(\d+)(?:\+[\w.-]+)?$/.exec(version);
|
|
54
|
+
return !!match && (Number(match[1]) > 0 || Number(match[2]) >= 264);
|
|
55
|
+
}
|
|
56
|
+
async function assertSearchContractAiSdkSupport() {
|
|
57
|
+
let version;
|
|
58
|
+
try {
|
|
59
|
+
const packageInfo = createRequire(import.meta.url)("ai/package.json");
|
|
60
|
+
version = packageInfo.version;
|
|
61
|
+
} catch {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"Cannot verify the AI SDK peer required by searchContractVersion: 1."
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (typeof version !== "string" || !supportsSearchContractUiMetadata(version)) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`searchContractVersion: 1 requires ai >=${SEARCH_CONTRACT_AI_SDK_MINIMUM} <7.0.0 to preserve structured tool-result metadata; found ai@${String(version)}.`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/provider/search-contract-discovery.ts
|
|
74
|
+
var CACHE_TTL_MS = 3e4;
|
|
75
|
+
var PROBE_TIMEOUT_MS = 4e3;
|
|
76
|
+
var MAX_ENDPOINTS_PER_HANDLE = 16;
|
|
77
|
+
var MAX_RESPONSE_BYTES = 16384;
|
|
78
|
+
var cache = /* @__PURE__ */ new WeakMap();
|
|
79
|
+
function abortError() {
|
|
80
|
+
return new DOMException("Search capability discovery aborted.", "AbortError");
|
|
81
|
+
}
|
|
82
|
+
function waitForSearchDiscovery(promise, signal) {
|
|
83
|
+
if (signal?.aborted) return Promise.reject(abortError());
|
|
84
|
+
if (!signal) return promise;
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const onAbort = () => reject(abortError());
|
|
87
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
88
|
+
promise.then(resolve, reject).finally(() => {
|
|
89
|
+
signal.removeEventListener("abort", onAbort);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function object(value) {
|
|
94
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
95
|
+
}
|
|
96
|
+
function parseHealth(text) {
|
|
97
|
+
const status = /\n(\d{3})$/.exec(text);
|
|
98
|
+
if (!status || status[1] !== "200") {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Search capability discovery failed (HTTP ${status?.[1] ?? "unknown"}).`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
let health;
|
|
104
|
+
try {
|
|
105
|
+
health = JSON.parse(text.slice(0, status.index));
|
|
106
|
+
} catch {
|
|
107
|
+
throw new Error("Invalid daemon health response during search discovery.");
|
|
108
|
+
}
|
|
109
|
+
const envelope = object(health);
|
|
110
|
+
const data = object(envelope?.data);
|
|
111
|
+
if (envelope?.ok !== true || data?.status !== "ok") {
|
|
112
|
+
throw new Error("Invalid daemon health response during search discovery.");
|
|
113
|
+
}
|
|
114
|
+
if (data.capabilities === void 0) return void 0;
|
|
115
|
+
const capabilities = object(data.capabilities);
|
|
116
|
+
if (!capabilities)
|
|
117
|
+
throw new Error("Invalid daemon capability advertisement.");
|
|
118
|
+
if (capabilities.searchContracts === void 0) return void 0;
|
|
119
|
+
const contracts = object(capabilities.searchContracts);
|
|
120
|
+
if (!contracts)
|
|
121
|
+
throw new Error("Invalid daemon search capability advertisement.");
|
|
122
|
+
if (contracts.pi === void 0) return void 0;
|
|
123
|
+
if (!isSearchContractCapabilities(contracts.pi)) {
|
|
124
|
+
throw new Error("Invalid Pi search capability advertisement.");
|
|
125
|
+
}
|
|
126
|
+
return contracts.pi.structured_retrieval && contracts.pi.explicit_source_marker_annotations ? 1 : void 0;
|
|
127
|
+
}
|
|
128
|
+
async function probe(handle, url, opts) {
|
|
129
|
+
const decoder = new TextDecoder();
|
|
130
|
+
let text = "";
|
|
131
|
+
let bytes = 0;
|
|
132
|
+
try {
|
|
133
|
+
for await (const chunk of handle.exec(
|
|
134
|
+
[
|
|
135
|
+
"curl",
|
|
136
|
+
"-sS",
|
|
137
|
+
"--connect-timeout",
|
|
138
|
+
"2",
|
|
139
|
+
"--max-time",
|
|
140
|
+
"4",
|
|
141
|
+
"--write-out",
|
|
142
|
+
"\n%{http_code}",
|
|
143
|
+
"--url",
|
|
144
|
+
url
|
|
145
|
+
],
|
|
146
|
+
opts
|
|
147
|
+
)) {
|
|
148
|
+
bytes += chunk.byteLength;
|
|
149
|
+
if (bytes > MAX_RESPONSE_BYTES) {
|
|
150
|
+
throw new Error("Daemon capability response exceeds its size limit.");
|
|
151
|
+
}
|
|
152
|
+
text += decoder.decode(chunk, { stream: true });
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
if (opts.signal?.aborted) throw opts.signal.reason ?? abortError();
|
|
156
|
+
throw new Error("Search capability discovery transport failed.");
|
|
157
|
+
}
|
|
158
|
+
text += decoder.decode();
|
|
159
|
+
return parseHealth(text.trimEnd());
|
|
160
|
+
}
|
|
161
|
+
async function join(flight, signal) {
|
|
162
|
+
flight.waiters += 1;
|
|
163
|
+
try {
|
|
164
|
+
return await waitForSearchDiscovery(flight.promise, signal);
|
|
165
|
+
} finally {
|
|
166
|
+
flight.waiters -= 1;
|
|
167
|
+
if (flight.waiters === 0 && !flight.settled) {
|
|
168
|
+
flight.controller.abort(abortError());
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function discoverPiSearchContract(handle, daemonUrl, opts = {}) {
|
|
173
|
+
if (opts.signal?.aborted) throw abortError();
|
|
174
|
+
const parsed = new URL(daemonUrl);
|
|
175
|
+
if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
176
|
+
throw new Error("Invalid daemon URL for search capability discovery.");
|
|
177
|
+
}
|
|
178
|
+
parsed.pathname = `${parsed.pathname.replace(/\/$/, "").replace(/\/healthz$/, "")}/healthz`;
|
|
179
|
+
const url = parsed.href;
|
|
180
|
+
const key = JSON.stringify([handle.getSandboxId(), url]);
|
|
181
|
+
let entries = cache.get(handle);
|
|
182
|
+
if (!entries) {
|
|
183
|
+
entries = /* @__PURE__ */ new Map();
|
|
184
|
+
cache.set(handle, entries);
|
|
185
|
+
}
|
|
186
|
+
const previous = entries.get(key);
|
|
187
|
+
if (previous?.flight) return join(previous.flight, opts.signal);
|
|
188
|
+
if (previous && previous.expiresAt > Date.now()) return previous.version;
|
|
189
|
+
entries.delete(key);
|
|
190
|
+
for (const [entryKey, entry2] of entries) {
|
|
191
|
+
if (!entry2.flight && entry2.expiresAt <= Date.now())
|
|
192
|
+
entries.delete(entryKey);
|
|
193
|
+
}
|
|
194
|
+
if (entries.size >= MAX_ENDPOINTS_PER_HANDLE) {
|
|
195
|
+
const evictable = [...entries].find(([, entry2]) => !entry2.flight);
|
|
196
|
+
if (!evictable)
|
|
197
|
+
throw new Error("Too many pending daemon capability probes.");
|
|
198
|
+
entries.delete(evictable[0]);
|
|
199
|
+
}
|
|
200
|
+
const controller = new AbortController();
|
|
201
|
+
const entry = { expiresAt: 0 };
|
|
202
|
+
const flight = {
|
|
203
|
+
controller,
|
|
204
|
+
promise: Promise.resolve(void 0),
|
|
205
|
+
waiters: 0,
|
|
206
|
+
settled: false
|
|
207
|
+
};
|
|
208
|
+
entry.flight = flight;
|
|
209
|
+
entries.set(key, entry);
|
|
210
|
+
const timer = setTimeout(() => {
|
|
211
|
+
controller.abort(
|
|
212
|
+
new DOMException(
|
|
213
|
+
"Search capability discovery timed out.",
|
|
214
|
+
"TimeoutError"
|
|
215
|
+
)
|
|
216
|
+
);
|
|
217
|
+
}, PROBE_TIMEOUT_MS);
|
|
218
|
+
const operation = probe(handle, url, {
|
|
219
|
+
cwd: opts.cwd ?? handle.getWorkdir(),
|
|
220
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
221
|
+
signal: controller.signal
|
|
222
|
+
});
|
|
223
|
+
operation.then(
|
|
224
|
+
(version) => {
|
|
225
|
+
flight.settled = true;
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
if (controller.signal.aborted) entries.delete(key);
|
|
228
|
+
else entries.set(key, { version, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
229
|
+
},
|
|
230
|
+
() => {
|
|
231
|
+
flight.settled = true;
|
|
232
|
+
clearTimeout(timer);
|
|
233
|
+
entries.delete(key);
|
|
234
|
+
}
|
|
235
|
+
);
|
|
236
|
+
flight.promise = new Promise((resolve, reject) => {
|
|
237
|
+
const onAbort = () => reject(controller.signal.reason ?? abortError());
|
|
238
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
239
|
+
operation.then(resolve, reject).finally(() => {
|
|
240
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
return join(flight, opts.signal);
|
|
244
|
+
}
|
|
245
|
+
|
|
43
246
|
// src/provider/tool-refs.ts
|
|
44
247
|
var BUNNY_PROVIDER_OPTIONS_KEY = "bunny-agent";
|
|
45
248
|
function bunnyHttpTool(input) {
|
|
@@ -361,7 +564,14 @@ function readToolDynamicFlag(parsed) {
|
|
|
361
564
|
}
|
|
362
565
|
return parsed.providerExecuted === true;
|
|
363
566
|
}
|
|
364
|
-
|
|
567
|
+
function mergeProviderMetadata(previous, next) {
|
|
568
|
+
const merged = { ...previous };
|
|
569
|
+
for (const [provider, fields] of Object.entries(next ?? {})) {
|
|
570
|
+
merged[provider] = { ...merged[provider], ...fields };
|
|
571
|
+
}
|
|
572
|
+
return merged;
|
|
573
|
+
}
|
|
574
|
+
var BunnyAgentLanguageModel = class _BunnyAgentLanguageModel {
|
|
365
575
|
specificationVersion = "v3";
|
|
366
576
|
provider;
|
|
367
577
|
modelId;
|
|
@@ -376,6 +586,68 @@ var BunnyAgentLanguageModel = class {
|
|
|
376
586
|
legacyTextPartCounter = 0;
|
|
377
587
|
receivedFinish = false;
|
|
378
588
|
activeReasoningPartId;
|
|
589
|
+
acknowledgedSearchContract;
|
|
590
|
+
/** Defense in depth: the runner already dedupes per emitted source, but a
|
|
591
|
+
* duplicate `source-url` event must never reach the UI as two source parts. */
|
|
592
|
+
emittedSourceIds = /* @__PURE__ */ new Set();
|
|
593
|
+
completedTextIds = /* @__PURE__ */ new Set();
|
|
594
|
+
completedToolIds = /* @__PURE__ */ new Set();
|
|
595
|
+
textById = /* @__PURE__ */ new Map();
|
|
596
|
+
textMetadata = /* @__PURE__ */ new Map();
|
|
597
|
+
toolMetadata = /* @__PURE__ */ new Map();
|
|
598
|
+
pendingSources = /* @__PURE__ */ new Map();
|
|
599
|
+
citedSources = /* @__PURE__ */ new Map();
|
|
600
|
+
messageMetadata = {};
|
|
601
|
+
seenMessageMetadata = /* @__PURE__ */ new Set();
|
|
602
|
+
seenStreamErrors = /* @__PURE__ */ new Set();
|
|
603
|
+
/** Merge every provider namespace while retaining negotiated request facts. */
|
|
604
|
+
partMetadata(raw, textPartId) {
|
|
605
|
+
const metadata = mergeProviderMetadata(
|
|
606
|
+
textPartId === void 0 ? void 0 : this.textMetadata.get(textPartId),
|
|
607
|
+
raw && typeof raw === "object" ? raw : void 0
|
|
608
|
+
);
|
|
609
|
+
const { searchContract: _untrustedAcknowledgement, ...bunnyMetadata } = metadata["bunny-agent"] ?? {};
|
|
610
|
+
const result = {
|
|
611
|
+
...metadata,
|
|
612
|
+
"bunny-agent": {
|
|
613
|
+
...this.messageMetadata,
|
|
614
|
+
...bunnyMetadata,
|
|
615
|
+
...this.sessionId ? { sessionId: this.sessionId } : {},
|
|
616
|
+
...textPartId !== void 0 ? { textPartId } : {},
|
|
617
|
+
...this.acknowledgedSearchContract ? {
|
|
618
|
+
searchContract: this.acknowledgedSearchContract
|
|
619
|
+
} : {}
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
if (textPartId !== void 0) this.textMetadata.set(textPartId, result);
|
|
623
|
+
return result;
|
|
624
|
+
}
|
|
625
|
+
toolPartMetadata(parsed) {
|
|
626
|
+
const id = parsed.toolCallId;
|
|
627
|
+
const metadata = mergeProviderMetadata(
|
|
628
|
+
this.toolMetadata.get(id),
|
|
629
|
+
this.partMetadata(parsed.providerMetadata)
|
|
630
|
+
);
|
|
631
|
+
this.toolMetadata.set(id, metadata);
|
|
632
|
+
return metadata;
|
|
633
|
+
}
|
|
634
|
+
emitCitedSource(sourceId) {
|
|
635
|
+
const source = this.pendingSources.get(sourceId);
|
|
636
|
+
const cited = this.citedSources.get(sourceId);
|
|
637
|
+
if (!source || !cited || this.emittedSourceIds.has(sourceId) || typeof source.url !== "string" || !isSafeHttpUrl(source.url) || new URL(source.url).href !== new URL(cited.url).href)
|
|
638
|
+
return [];
|
|
639
|
+
this.emittedSourceIds.add(sourceId);
|
|
640
|
+
return [
|
|
641
|
+
{
|
|
642
|
+
type: "source",
|
|
643
|
+
sourceType: "url",
|
|
644
|
+
id: sourceId,
|
|
645
|
+
url: source.url,
|
|
646
|
+
title: cited.title,
|
|
647
|
+
...source.providerMetadata ? { providerMetadata: this.partMetadata(source.providerMetadata) } : {}
|
|
648
|
+
}
|
|
649
|
+
];
|
|
650
|
+
}
|
|
379
651
|
logUnparsedStreamLine(candidate, error) {
|
|
380
652
|
const trimmed = candidate.trim();
|
|
381
653
|
if (!trimmed) return;
|
|
@@ -421,13 +693,20 @@ var BunnyAgentLanguageModel = class {
|
|
|
421
693
|
if (done) break;
|
|
422
694
|
switch (value.type) {
|
|
423
695
|
case "text-start": {
|
|
424
|
-
textParts.set(value.id, {
|
|
696
|
+
textParts.set(value.id, {
|
|
697
|
+
text: "",
|
|
698
|
+
providerMetadata: value.providerMetadata
|
|
699
|
+
});
|
|
425
700
|
break;
|
|
426
701
|
}
|
|
427
702
|
case "text-delta": {
|
|
428
703
|
const part = textParts.get(value.id);
|
|
429
704
|
if (part) {
|
|
430
705
|
part.text += value.delta;
|
|
706
|
+
part.providerMetadata = mergeProviderMetadata(
|
|
707
|
+
part.providerMetadata,
|
|
708
|
+
value.providerMetadata
|
|
709
|
+
);
|
|
431
710
|
}
|
|
432
711
|
break;
|
|
433
712
|
}
|
|
@@ -436,8 +715,13 @@ var BunnyAgentLanguageModel = class {
|
|
|
436
715
|
if (part) {
|
|
437
716
|
content.push({
|
|
438
717
|
type: "text",
|
|
439
|
-
text: part.text
|
|
718
|
+
text: part.text,
|
|
719
|
+
providerMetadata: mergeProviderMetadata(
|
|
720
|
+
part.providerMetadata,
|
|
721
|
+
value.providerMetadata
|
|
722
|
+
)
|
|
440
723
|
});
|
|
724
|
+
textParts.delete(value.id);
|
|
441
725
|
}
|
|
442
726
|
break;
|
|
443
727
|
}
|
|
@@ -477,13 +761,15 @@ var BunnyAgentLanguageModel = class {
|
|
|
477
761
|
break;
|
|
478
762
|
}
|
|
479
763
|
case "tool-call": {
|
|
480
|
-
content.push(
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
764
|
+
content.push(value);
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
case "tool-result": {
|
|
768
|
+
content.push(value);
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
case "source": {
|
|
772
|
+
content.push(value);
|
|
487
773
|
break;
|
|
488
774
|
}
|
|
489
775
|
case "stream-start": {
|
|
@@ -517,9 +803,52 @@ var BunnyAgentLanguageModel = class {
|
|
|
517
803
|
};
|
|
518
804
|
}
|
|
519
805
|
async doStream(options) {
|
|
806
|
+
let invocationOptions = this.options;
|
|
807
|
+
let discoveredHandle;
|
|
808
|
+
if (this.options.searchContractVersion === "auto") {
|
|
809
|
+
if (!this.options.sandbox) {
|
|
810
|
+
throw new Error(
|
|
811
|
+
"BunnyAgent language model requires a sandbox adapter (set `sandbox` on the provider)."
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
if (options.abortSignal?.aborted) {
|
|
815
|
+
throw new DOMException(
|
|
816
|
+
"Search capability discovery aborted.",
|
|
817
|
+
"AbortError"
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
let version;
|
|
821
|
+
if (this.options.runner.runnerType === "pi" && this.options.daemonUrl) {
|
|
822
|
+
discoveredHandle = await waitForSearchDiscovery(
|
|
823
|
+
this.options.sandbox.attach(),
|
|
824
|
+
options.abortSignal
|
|
825
|
+
);
|
|
826
|
+
version = await discoverPiSearchContract(
|
|
827
|
+
discoveredHandle,
|
|
828
|
+
this.options.daemonUrl,
|
|
829
|
+
{
|
|
830
|
+
cwd: this.options.cwd,
|
|
831
|
+
signal: options.abortSignal
|
|
832
|
+
}
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
invocationOptions = {
|
|
836
|
+
...this.options,
|
|
837
|
+
runner: { ...this.options.runner, searchContractVersion: version }
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
const invocation = new _BunnyAgentLanguageModel({
|
|
841
|
+
id: this.modelId,
|
|
842
|
+
options: invocationOptions
|
|
843
|
+
});
|
|
844
|
+
return invocation.doStreamInvocation(options, discoveredHandle);
|
|
845
|
+
}
|
|
846
|
+
async doStreamInvocation(options, discoveredHandle) {
|
|
847
|
+
if (this.requestedSearchContractVersion === 1) {
|
|
848
|
+
await assertSearchContractAiSdkSupport();
|
|
849
|
+
}
|
|
520
850
|
const { prompt, abortSignal } = options;
|
|
521
851
|
const messages = this.convertPromptToMessages(prompt);
|
|
522
|
-
this.resetStreamState();
|
|
523
852
|
this.logger.debug(
|
|
524
853
|
`[bunny-agent] Starting stream with ${messages.length} messages`
|
|
525
854
|
);
|
|
@@ -533,7 +862,7 @@ var BunnyAgentLanguageModel = class {
|
|
|
533
862
|
const toolRefs = mergeToolRefs(this.options.toolRefs, callToolRefs);
|
|
534
863
|
const daemonUrl = this.options.daemonUrl;
|
|
535
864
|
if (daemonUrl) {
|
|
536
|
-
const handle = await sandbox.attach();
|
|
865
|
+
const handle = discoveredHandle ?? await sandbox.attach();
|
|
537
866
|
const sandboxEnv2 = sandbox.getEnv?.() ?? {};
|
|
538
867
|
const runnerEnv = { ...sandboxEnv2, ...this.options.env };
|
|
539
868
|
const body = {
|
|
@@ -547,7 +876,12 @@ var BunnyAgentLanguageModel = class {
|
|
|
547
876
|
};
|
|
548
877
|
const execOpts = {
|
|
549
878
|
cwd: this.options.cwd ?? handle.getWorkdir(),
|
|
550
|
-
signal: abortSignal
|
|
879
|
+
signal: abortSignal,
|
|
880
|
+
// A coding run lasts as long as the agent works and ends via
|
|
881
|
+
// `abortSignal`. On exec-based sandboxes (LocalMachine), the daemon
|
|
882
|
+
// call would otherwise inherit the default command timeout (5 min)
|
|
883
|
+
// and kill every longer run. Sandock streams natively and ignores it.
|
|
884
|
+
timeout: 0
|
|
551
885
|
};
|
|
552
886
|
const iterable = streamCodingRunFromSandbox(
|
|
553
887
|
handle,
|
|
@@ -601,12 +935,36 @@ var BunnyAgentLanguageModel = class {
|
|
|
601
935
|
throw error;
|
|
602
936
|
}
|
|
603
937
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
this.
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
938
|
+
/** The `searchContractVersion` explicitly requested for this turn, if any (never a process-global default). */
|
|
939
|
+
get requestedSearchContractVersion() {
|
|
940
|
+
return this.options.runner.searchContractVersion === 1 ? 1 : void 0;
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* When an explicit v1 selection was requested but the stream reached a
|
|
944
|
+
* normal finish without ever delivering a `searchContract` acknowledgement
|
|
945
|
+
* (e.g. an older daemon/CLI peer that silently ignores the field), surface
|
|
946
|
+
* a clear error instead of letting the caller believe v1 behavior is
|
|
947
|
+
* active. Never retried automatically — the caller decides whether to
|
|
948
|
+
* retry without the contract.
|
|
949
|
+
*/
|
|
950
|
+
buildMissingAcknowledgementErrorParts() {
|
|
951
|
+
return [
|
|
952
|
+
{
|
|
953
|
+
type: "error",
|
|
954
|
+
error: new Error(
|
|
955
|
+
"searchContractVersion: 1 was explicitly requested but the runner/daemon never acknowledged support for it (no searchContract capability message received). The peer may be running an older release that silently ignores this option."
|
|
956
|
+
)
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
type: "finish",
|
|
960
|
+
finishReason: {
|
|
961
|
+
unified: "error",
|
|
962
|
+
raw: "search-contract-unacknowledged"
|
|
963
|
+
},
|
|
964
|
+
usage: createEmptyUsage(),
|
|
965
|
+
providerMetadata: this.partMetadata(void 0)
|
|
966
|
+
}
|
|
967
|
+
];
|
|
610
968
|
}
|
|
611
969
|
/** Close the open reasoning part, if any (returns the end part to emit). */
|
|
612
970
|
closeReasoningPart() {
|
|
@@ -617,6 +975,9 @@ var BunnyAgentLanguageModel = class {
|
|
|
617
975
|
}
|
|
618
976
|
/** Synthesize a terminal error+finish pair for streams that end (EOF or `[DONE]`) without ever emitting a "finish" part, so the UI shows an error instead of silently stalling. */
|
|
619
977
|
buildUnexpectedEndParts() {
|
|
978
|
+
if (this.requestedSearchContractVersion === 1 && this.acknowledgedSearchContract === void 0) {
|
|
979
|
+
return this.buildMissingAcknowledgementErrorParts();
|
|
980
|
+
}
|
|
620
981
|
return [
|
|
621
982
|
{
|
|
622
983
|
type: "error",
|
|
@@ -628,11 +989,7 @@ var BunnyAgentLanguageModel = class {
|
|
|
628
989
|
type: "finish",
|
|
629
990
|
finishReason: { unified: "error", raw: "unexpected-stream-end" },
|
|
630
991
|
usage: createEmptyUsage(),
|
|
631
|
-
providerMetadata:
|
|
632
|
-
"bunny-agent": {
|
|
633
|
-
sessionId: this.sessionId
|
|
634
|
-
}
|
|
635
|
-
}
|
|
992
|
+
providerMetadata: this.partMetadata(void 0)
|
|
636
993
|
}
|
|
637
994
|
];
|
|
638
995
|
}
|
|
@@ -663,7 +1020,8 @@ var BunnyAgentLanguageModel = class {
|
|
|
663
1020
|
yolo: this.options.yolo ?? (runner.runnerType === "pi" ? true : void 0),
|
|
664
1021
|
...toolRefs && toolRefs.length > 0 ? { toolRefs } : {},
|
|
665
1022
|
...this.options.mcpConfig ? { mcpConfig: this.options.mcpConfig } : {},
|
|
666
|
-
...this.options.effort ? { effort: this.options.effort } : {}
|
|
1023
|
+
...this.options.effort ? { effort: this.options.effort } : {},
|
|
1024
|
+
...runner.searchContractVersion !== void 0 ? { searchContractVersion: runner.searchContractVersion } : {}
|
|
667
1025
|
};
|
|
668
1026
|
}
|
|
669
1027
|
buildStreamResult(bytesStream, messages, signal) {
|
|
@@ -681,9 +1039,11 @@ var BunnyAgentLanguageModel = class {
|
|
|
681
1039
|
async start(controller) {
|
|
682
1040
|
try {
|
|
683
1041
|
let buffer = "";
|
|
1042
|
+
const decoder = new TextDecoder();
|
|
684
1043
|
while (true) {
|
|
685
1044
|
const { done, value } = await reader.read();
|
|
686
1045
|
if (done) {
|
|
1046
|
+
buffer += decoder.decode();
|
|
687
1047
|
if (buffer.trim()) {
|
|
688
1048
|
const parts = self.parseSSEBuffer(buffer);
|
|
689
1049
|
for (const part of parts) {
|
|
@@ -698,8 +1058,7 @@ var BunnyAgentLanguageModel = class {
|
|
|
698
1058
|
controller.close();
|
|
699
1059
|
break;
|
|
700
1060
|
}
|
|
701
|
-
|
|
702
|
-
buffer += text;
|
|
1061
|
+
buffer += decoder.decode(value, { stream: true });
|
|
703
1062
|
const lines = buffer.split("\n");
|
|
704
1063
|
buffer = lines.pop() ?? "";
|
|
705
1064
|
let foundDone = false;
|
|
@@ -792,8 +1151,11 @@ var BunnyAgentLanguageModel = class {
|
|
|
792
1151
|
const parts = [];
|
|
793
1152
|
const parsed = JSON.parse(data);
|
|
794
1153
|
const parsedType = typeof parsed.type === "string" ? parsed.type : void 0;
|
|
1154
|
+
if (this.receivedFinish && parsedType !== "error" && !(!parsedType && typeof parsed.error === "string"))
|
|
1155
|
+
return parts;
|
|
795
1156
|
if (!parsedType && typeof parsed.error === "string") {
|
|
796
|
-
this.
|
|
1157
|
+
if (this.seenStreamErrors.has(parsed.error)) return parts;
|
|
1158
|
+
this.seenStreamErrors.add(parsed.error);
|
|
797
1159
|
return [
|
|
798
1160
|
{
|
|
799
1161
|
type: "error",
|
|
@@ -814,21 +1176,31 @@ var BunnyAgentLanguageModel = class {
|
|
|
814
1176
|
{
|
|
815
1177
|
type: "text-start",
|
|
816
1178
|
id,
|
|
817
|
-
providerMetadata:
|
|
818
|
-
"bunny-agent": {
|
|
819
|
-
sessionId: this.sessionId
|
|
820
|
-
}
|
|
821
|
-
}
|
|
1179
|
+
providerMetadata: this.partMetadata(parsed.providerMetadata, id)
|
|
822
1180
|
},
|
|
823
1181
|
{ type: "text-delta", id, delta: text },
|
|
824
|
-
{
|
|
1182
|
+
{
|
|
1183
|
+
type: "text-end",
|
|
1184
|
+
id,
|
|
1185
|
+
providerMetadata: this.partMetadata(parsed.providerMetadata, id)
|
|
1186
|
+
}
|
|
825
1187
|
);
|
|
826
1188
|
}
|
|
827
1189
|
break;
|
|
828
1190
|
}
|
|
829
1191
|
case "message-metadata": {
|
|
830
1192
|
const metadata = parsed.messageMetadata;
|
|
831
|
-
|
|
1193
|
+
const key = JSON.stringify(metadata);
|
|
1194
|
+
if (this.seenMessageMetadata.has(key)) break;
|
|
1195
|
+
this.seenMessageMetadata.add(key);
|
|
1196
|
+
if (metadata && typeof metadata === "object") {
|
|
1197
|
+
const { searchContract: _acknowledgement, ...fields } = metadata;
|
|
1198
|
+
this.messageMetadata = {
|
|
1199
|
+
...this.messageMetadata,
|
|
1200
|
+
...fields
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
if (metadata?.sessionId && typeof metadata.sessionId === "string" && this.sessionId !== metadata.sessionId) {
|
|
832
1204
|
this.sessionId = metadata.sessionId;
|
|
833
1205
|
this.logger.debug(
|
|
834
1206
|
`[bunny-agent] Session ID extracted: ${this.sessionId}`
|
|
@@ -838,6 +1210,12 @@ var BunnyAgentLanguageModel = class {
|
|
|
838
1210
|
rawValue: this.sessionId
|
|
839
1211
|
});
|
|
840
1212
|
}
|
|
1213
|
+
if (this.requestedSearchContractVersion === 1 && isSearchContractCapabilities(metadata?.searchContract) && this.acknowledgedSearchContract === void 0) {
|
|
1214
|
+
this.acknowledgedSearchContract = metadata.searchContract;
|
|
1215
|
+
this.logger.debug(
|
|
1216
|
+
`[bunny-agent] Search contract acknowledged: ${JSON.stringify(this.acknowledgedSearchContract)}`
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
841
1219
|
break;
|
|
842
1220
|
}
|
|
843
1221
|
// Context compaction progress. Delivered out-of-band via the
|
|
@@ -877,30 +1255,83 @@ var BunnyAgentLanguageModel = class {
|
|
|
877
1255
|
break;
|
|
878
1256
|
}
|
|
879
1257
|
case "text-start": {
|
|
1258
|
+
const id = parsed.id;
|
|
1259
|
+
if (this.textById.has(id) || this.completedTextIds.has(id)) break;
|
|
1260
|
+
this.textById.set(id, "");
|
|
880
1261
|
parts.push(...this.closeReasoningPart());
|
|
881
1262
|
parts.push({
|
|
882
1263
|
type: "text-start",
|
|
883
1264
|
id: parsed.id,
|
|
884
|
-
providerMetadata:
|
|
885
|
-
"bunny-agent": {
|
|
886
|
-
sessionId: this.sessionId
|
|
887
|
-
}
|
|
888
|
-
}
|
|
1265
|
+
providerMetadata: this.partMetadata(parsed.providerMetadata, id)
|
|
889
1266
|
});
|
|
890
1267
|
break;
|
|
891
1268
|
}
|
|
892
1269
|
case "text-delta": {
|
|
1270
|
+
const id = parsed.id;
|
|
1271
|
+
if (this.completedTextIds.has(id)) break;
|
|
1272
|
+
this.textById.set(
|
|
1273
|
+
id,
|
|
1274
|
+
(this.textById.get(id) ?? "") + parsed.delta
|
|
1275
|
+
);
|
|
893
1276
|
parts.push({
|
|
894
1277
|
type: "text-delta",
|
|
895
1278
|
id: parsed.id,
|
|
896
|
-
delta: parsed.delta
|
|
1279
|
+
delta: parsed.delta,
|
|
1280
|
+
...parsed.providerMetadata ? {
|
|
1281
|
+
providerMetadata: this.partMetadata(
|
|
1282
|
+
parsed.providerMetadata,
|
|
1283
|
+
id
|
|
1284
|
+
)
|
|
1285
|
+
} : {}
|
|
897
1286
|
});
|
|
898
1287
|
break;
|
|
899
1288
|
}
|
|
900
1289
|
case "text-end": {
|
|
1290
|
+
const id = parsed.id;
|
|
1291
|
+
if (this.completedTextIds.has(id)) break;
|
|
1292
|
+
this.completedTextIds.add(id);
|
|
1293
|
+
const rawMetadata = parsed.providerMetadata;
|
|
1294
|
+
const bunnyMetadata = rawMetadata?.["bunny-agent"];
|
|
1295
|
+
const contract = bunnyMetadata?.citation_contract;
|
|
1296
|
+
if (this.acknowledgedSearchContract && isValidCitationContractMetadata(contract, {
|
|
1297
|
+
text: this.textById.get(id) ?? ""
|
|
1298
|
+
})) {
|
|
1299
|
+
for (const binding of contract.bindings) {
|
|
1300
|
+
const annotation = contract.annotations[binding.annotation_index];
|
|
1301
|
+
this.citedSources.set(binding.source_id, {
|
|
1302
|
+
url: annotation.url,
|
|
1303
|
+
title: annotation.title
|
|
1304
|
+
});
|
|
1305
|
+
parts.push(...this.emitCitedSource(binding.source_id));
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
901
1308
|
parts.push({
|
|
902
1309
|
type: "text-end",
|
|
903
|
-
id: parsed.id
|
|
1310
|
+
id: parsed.id,
|
|
1311
|
+
providerMetadata: this.partMetadata(rawMetadata, id)
|
|
1312
|
+
});
|
|
1313
|
+
break;
|
|
1314
|
+
}
|
|
1315
|
+
// Terminal per-text citation marker bridge: one `source-url` chunk per
|
|
1316
|
+
// actually cited source (never per retrieved-but-uncited result).
|
|
1317
|
+
case "source-url": {
|
|
1318
|
+
const sourceId = parsed.sourceId;
|
|
1319
|
+
if (typeof sourceId !== "string" || typeof parsed.url !== "string" || !isSafeHttpUrl(parsed.url))
|
|
1320
|
+
break;
|
|
1321
|
+
if (this.requestedSearchContractVersion === 1) {
|
|
1322
|
+
if (!this.pendingSources.has(sourceId))
|
|
1323
|
+
this.pendingSources.set(sourceId, parsed);
|
|
1324
|
+
parts.push(...this.emitCitedSource(sourceId));
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
if (this.emittedSourceIds.has(sourceId)) break;
|
|
1328
|
+
this.emittedSourceIds.add(sourceId);
|
|
1329
|
+
parts.push({
|
|
1330
|
+
type: "source",
|
|
1331
|
+
sourceType: "url",
|
|
1332
|
+
id: sourceId,
|
|
1333
|
+
url: parsed.url,
|
|
1334
|
+
...typeof parsed.title === "string" ? { title: parsed.title } : {}
|
|
904
1335
|
});
|
|
905
1336
|
break;
|
|
906
1337
|
}
|
|
@@ -911,7 +1342,8 @@ var BunnyAgentLanguageModel = class {
|
|
|
911
1342
|
id: parsed.toolCallId,
|
|
912
1343
|
toolName: parsed.toolName,
|
|
913
1344
|
dynamic: readToolDynamicFlag(parsed),
|
|
914
|
-
providerExecuted: parsed.providerExecuted
|
|
1345
|
+
providerExecuted: parsed.providerExecuted,
|
|
1346
|
+
providerMetadata: this.toolPartMetadata(parsed)
|
|
915
1347
|
});
|
|
916
1348
|
break;
|
|
917
1349
|
}
|
|
@@ -919,7 +1351,16 @@ var BunnyAgentLanguageModel = class {
|
|
|
919
1351
|
parts.push({
|
|
920
1352
|
type: "tool-input-delta",
|
|
921
1353
|
id: parsed.toolCallId,
|
|
922
|
-
delta: parsed.inputTextDelta
|
|
1354
|
+
delta: parsed.inputTextDelta,
|
|
1355
|
+
providerMetadata: this.toolPartMetadata(parsed)
|
|
1356
|
+
});
|
|
1357
|
+
break;
|
|
1358
|
+
}
|
|
1359
|
+
case "tool-input-end": {
|
|
1360
|
+
parts.push({
|
|
1361
|
+
type: "tool-input-end",
|
|
1362
|
+
id: parsed.toolCallId,
|
|
1363
|
+
providerMetadata: this.toolPartMetadata(parsed)
|
|
923
1364
|
});
|
|
924
1365
|
break;
|
|
925
1366
|
}
|
|
@@ -934,11 +1375,14 @@ var BunnyAgentLanguageModel = class {
|
|
|
934
1375
|
toolName,
|
|
935
1376
|
input: JSON.stringify(input),
|
|
936
1377
|
dynamic: readToolDynamicFlag(parsed),
|
|
937
|
-
providerExecuted: parsed.providerExecuted
|
|
1378
|
+
providerExecuted: parsed.providerExecuted,
|
|
1379
|
+
providerMetadata: this.toolPartMetadata(parsed)
|
|
938
1380
|
});
|
|
939
1381
|
break;
|
|
940
1382
|
}
|
|
941
1383
|
case "tool-output-available": {
|
|
1384
|
+
if (this.completedToolIds.has(parsed.toolCallId)) break;
|
|
1385
|
+
this.completedToolIds.add(parsed.toolCallId);
|
|
942
1386
|
const toolName = this.toolNameMap.get(parsed.toolCallId);
|
|
943
1387
|
parts.push({
|
|
944
1388
|
type: "tool-result",
|
|
@@ -946,11 +1390,14 @@ var BunnyAgentLanguageModel = class {
|
|
|
946
1390
|
toolName: toolName ?? "",
|
|
947
1391
|
result: parsed.output,
|
|
948
1392
|
isError: parsed.isError,
|
|
949
|
-
dynamic: readToolDynamicFlag(parsed)
|
|
1393
|
+
dynamic: readToolDynamicFlag(parsed),
|
|
1394
|
+
providerMetadata: this.toolPartMetadata(parsed)
|
|
950
1395
|
});
|
|
951
1396
|
break;
|
|
952
1397
|
}
|
|
953
1398
|
case "tool-output-error": {
|
|
1399
|
+
if (this.completedToolIds.has(parsed.toolCallId)) break;
|
|
1400
|
+
this.completedToolIds.add(parsed.toolCallId);
|
|
954
1401
|
const toolName = this.toolNameMap.get(parsed.toolCallId);
|
|
955
1402
|
parts.push({
|
|
956
1403
|
type: "tool-result",
|
|
@@ -958,17 +1405,18 @@ var BunnyAgentLanguageModel = class {
|
|
|
958
1405
|
toolName: toolName ?? "",
|
|
959
1406
|
result: parsed.errorText ?? "Tool execution failed",
|
|
960
1407
|
isError: true,
|
|
961
|
-
dynamic: readToolDynamicFlag(parsed)
|
|
1408
|
+
dynamic: readToolDynamicFlag(parsed),
|
|
1409
|
+
providerMetadata: this.toolPartMetadata(parsed)
|
|
962
1410
|
});
|
|
963
1411
|
break;
|
|
964
1412
|
}
|
|
965
1413
|
case "error": {
|
|
966
|
-
|
|
1414
|
+
const message = parsed.errorText || parsed.error || "Unknown stream error";
|
|
1415
|
+
if (this.seenStreamErrors.has(message)) break;
|
|
1416
|
+
this.seenStreamErrors.add(message);
|
|
967
1417
|
parts.push({
|
|
968
1418
|
type: "error",
|
|
969
|
-
error: new Error(
|
|
970
|
-
parsed.errorText || parsed.error || "Unknown stream error"
|
|
971
|
-
)
|
|
1419
|
+
error: new Error(message)
|
|
972
1420
|
});
|
|
973
1421
|
break;
|
|
974
1422
|
}
|
|
@@ -982,6 +1430,10 @@ var BunnyAgentLanguageModel = class {
|
|
|
982
1430
|
} else {
|
|
983
1431
|
finishReason = this.mapFinishReason(rawFinishReason);
|
|
984
1432
|
}
|
|
1433
|
+
if (this.requestedSearchContractVersion === 1 && this.acknowledgedSearchContract === void 0 && finishReason.unified !== "error") {
|
|
1434
|
+
parts.push(...this.buildMissingAcknowledgementErrorParts());
|
|
1435
|
+
break;
|
|
1436
|
+
}
|
|
985
1437
|
const usage = normalizeBunnyAgentUsage(
|
|
986
1438
|
parsed.messageMetadata ?? void 0
|
|
987
1439
|
) ?? createEmptyUsage();
|
|
@@ -989,12 +1441,14 @@ var BunnyAgentLanguageModel = class {
|
|
|
989
1441
|
type: "finish",
|
|
990
1442
|
finishReason,
|
|
991
1443
|
usage,
|
|
992
|
-
providerMetadata:
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1444
|
+
providerMetadata: this.partMetadata(
|
|
1445
|
+
mergeProviderMetadata(
|
|
1446
|
+
parsed.providerMetadata,
|
|
1447
|
+
{
|
|
1448
|
+
"bunny-agent": parsed.messageMetadata ?? {}
|
|
1449
|
+
}
|
|
1450
|
+
)
|
|
1451
|
+
)
|
|
998
1452
|
});
|
|
999
1453
|
break;
|
|
1000
1454
|
}
|
|
@@ -1090,27 +1544,31 @@ function createBunnyAgent(defaultOptions) {
|
|
|
1090
1544
|
const logger = getProviderLogger(defaultOptions);
|
|
1091
1545
|
if (!defaultOptions.sandbox) {
|
|
1092
1546
|
throw new Error(
|
|
1093
|
-
"Provide a `sandbox` adapter (e.g. E2BSandbox, LocalMachine). Optional `daemonUrl` uses in-sandbox HTTP to bunny-agent-daemon (no automatic `/healthz` probe). Use `isBunnyAgentDaemonHealthy` from `@bunny-agent/sdk` if you want to probe and omit `daemonUrl` for CLI fallback. Omit `daemonUrl` to always use CLI."
|
|
1547
|
+
"Provide a `sandbox` adapter (e.g. E2BSandbox, LocalMachine). Optional `daemonUrl` uses in-sandbox HTTP to bunny-agent-daemon (no automatic `/healthz` probe unless searchContractVersion is `auto`). Use `isBunnyAgentDaemonHealthy` from `@bunny-agent/sdk` if you want to probe and omit `daemonUrl` for CLI fallback. Omit `daemonUrl` to always use CLI."
|
|
1094
1548
|
);
|
|
1095
1549
|
}
|
|
1096
1550
|
const createModel = (modelId, options = {}) => {
|
|
1097
1551
|
const mergedSkillPaths = options.skillPaths !== void 0 ? options.skillPaths : defaultOptions.skillPaths;
|
|
1098
1552
|
const mergedAllowedTools = options.allowedTools !== void 0 ? options.allowedTools : defaultOptions.allowedTools;
|
|
1553
|
+
const searchContractVersion = options.searchContractVersion ?? defaultOptions.searchContractVersion;
|
|
1099
1554
|
const runner = {
|
|
1100
1555
|
model: modelId,
|
|
1101
1556
|
runnerType: options.runnerType ?? defaultOptions.runnerType,
|
|
1102
1557
|
outputFormat: "stream",
|
|
1103
1558
|
maxTurns: options.maxTurns ?? defaultOptions.maxTurns,
|
|
1559
|
+
...(options.yolo ?? defaultOptions.yolo) !== void 0 ? { yolo: options.yolo ?? defaultOptions.yolo } : {},
|
|
1104
1560
|
...options.systemPrompt ?? defaultOptions.systemPrompt ? { systemPrompt: options.systemPrompt ?? defaultOptions.systemPrompt } : {},
|
|
1105
1561
|
...mergedSkillPaths && mergedSkillPaths.length > 0 ? { skillPaths: mergedSkillPaths } : {},
|
|
1106
1562
|
...mergedAllowedTools !== void 0 ? { allowedTools: mergedAllowedTools } : {},
|
|
1107
1563
|
...options.effort ?? defaultOptions.effort ? {
|
|
1108
1564
|
effort: options.effort ?? defaultOptions.effort
|
|
1109
|
-
} : {}
|
|
1565
|
+
} : {},
|
|
1566
|
+
...searchContractVersion === 1 ? { searchContractVersion: 1 } : {}
|
|
1110
1567
|
};
|
|
1111
1568
|
const mergedOptions = {
|
|
1112
1569
|
...defaultOptions,
|
|
1113
1570
|
...options,
|
|
1571
|
+
searchContractVersion,
|
|
1114
1572
|
runner,
|
|
1115
1573
|
env: {
|
|
1116
1574
|
...defaultOptions.env,
|