@bunny-agent/sdk 0.9.63-beta.2 → 0.10.0-beta.3
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 +508 -56
- 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 = {
|
|
@@ -606,12 +935,36 @@ var BunnyAgentLanguageModel = class {
|
|
|
606
935
|
throw error;
|
|
607
936
|
}
|
|
608
937
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
this.
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
+
];
|
|
615
968
|
}
|
|
616
969
|
/** Close the open reasoning part, if any (returns the end part to emit). */
|
|
617
970
|
closeReasoningPart() {
|
|
@@ -622,6 +975,9 @@ var BunnyAgentLanguageModel = class {
|
|
|
622
975
|
}
|
|
623
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. */
|
|
624
977
|
buildUnexpectedEndParts() {
|
|
978
|
+
if (this.requestedSearchContractVersion === 1 && this.acknowledgedSearchContract === void 0) {
|
|
979
|
+
return this.buildMissingAcknowledgementErrorParts();
|
|
980
|
+
}
|
|
625
981
|
return [
|
|
626
982
|
{
|
|
627
983
|
type: "error",
|
|
@@ -633,11 +989,7 @@ var BunnyAgentLanguageModel = class {
|
|
|
633
989
|
type: "finish",
|
|
634
990
|
finishReason: { unified: "error", raw: "unexpected-stream-end" },
|
|
635
991
|
usage: createEmptyUsage(),
|
|
636
|
-
providerMetadata:
|
|
637
|
-
"bunny-agent": {
|
|
638
|
-
sessionId: this.sessionId
|
|
639
|
-
}
|
|
640
|
-
}
|
|
992
|
+
providerMetadata: this.partMetadata(void 0)
|
|
641
993
|
}
|
|
642
994
|
];
|
|
643
995
|
}
|
|
@@ -668,7 +1020,8 @@ var BunnyAgentLanguageModel = class {
|
|
|
668
1020
|
yolo: this.options.yolo ?? (runner.runnerType === "pi" ? true : void 0),
|
|
669
1021
|
...toolRefs && toolRefs.length > 0 ? { toolRefs } : {},
|
|
670
1022
|
...this.options.mcpConfig ? { mcpConfig: this.options.mcpConfig } : {},
|
|
671
|
-
...this.options.effort ? { effort: this.options.effort } : {}
|
|
1023
|
+
...this.options.effort ? { effort: this.options.effort } : {},
|
|
1024
|
+
...runner.searchContractVersion !== void 0 ? { searchContractVersion: runner.searchContractVersion } : {}
|
|
672
1025
|
};
|
|
673
1026
|
}
|
|
674
1027
|
buildStreamResult(bytesStream, messages, signal) {
|
|
@@ -798,8 +1151,11 @@ var BunnyAgentLanguageModel = class {
|
|
|
798
1151
|
const parts = [];
|
|
799
1152
|
const parsed = JSON.parse(data);
|
|
800
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;
|
|
801
1156
|
if (!parsedType && typeof parsed.error === "string") {
|
|
802
|
-
this.
|
|
1157
|
+
if (this.seenStreamErrors.has(parsed.error)) return parts;
|
|
1158
|
+
this.seenStreamErrors.add(parsed.error);
|
|
803
1159
|
return [
|
|
804
1160
|
{
|
|
805
1161
|
type: "error",
|
|
@@ -820,21 +1176,31 @@ var BunnyAgentLanguageModel = class {
|
|
|
820
1176
|
{
|
|
821
1177
|
type: "text-start",
|
|
822
1178
|
id,
|
|
823
|
-
providerMetadata:
|
|
824
|
-
"bunny-agent": {
|
|
825
|
-
sessionId: this.sessionId
|
|
826
|
-
}
|
|
827
|
-
}
|
|
1179
|
+
providerMetadata: this.partMetadata(parsed.providerMetadata, id)
|
|
828
1180
|
},
|
|
829
1181
|
{ type: "text-delta", id, delta: text },
|
|
830
|
-
{
|
|
1182
|
+
{
|
|
1183
|
+
type: "text-end",
|
|
1184
|
+
id,
|
|
1185
|
+
providerMetadata: this.partMetadata(parsed.providerMetadata, id)
|
|
1186
|
+
}
|
|
831
1187
|
);
|
|
832
1188
|
}
|
|
833
1189
|
break;
|
|
834
1190
|
}
|
|
835
1191
|
case "message-metadata": {
|
|
836
1192
|
const metadata = parsed.messageMetadata;
|
|
837
|
-
|
|
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) {
|
|
838
1204
|
this.sessionId = metadata.sessionId;
|
|
839
1205
|
this.logger.debug(
|
|
840
1206
|
`[bunny-agent] Session ID extracted: ${this.sessionId}`
|
|
@@ -844,6 +1210,12 @@ var BunnyAgentLanguageModel = class {
|
|
|
844
1210
|
rawValue: this.sessionId
|
|
845
1211
|
});
|
|
846
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
|
+
}
|
|
847
1219
|
break;
|
|
848
1220
|
}
|
|
849
1221
|
// Context compaction progress. Delivered out-of-band via the
|
|
@@ -883,30 +1255,83 @@ var BunnyAgentLanguageModel = class {
|
|
|
883
1255
|
break;
|
|
884
1256
|
}
|
|
885
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, "");
|
|
886
1261
|
parts.push(...this.closeReasoningPart());
|
|
887
1262
|
parts.push({
|
|
888
1263
|
type: "text-start",
|
|
889
1264
|
id: parsed.id,
|
|
890
|
-
providerMetadata:
|
|
891
|
-
"bunny-agent": {
|
|
892
|
-
sessionId: this.sessionId
|
|
893
|
-
}
|
|
894
|
-
}
|
|
1265
|
+
providerMetadata: this.partMetadata(parsed.providerMetadata, id)
|
|
895
1266
|
});
|
|
896
1267
|
break;
|
|
897
1268
|
}
|
|
898
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
|
+
);
|
|
899
1276
|
parts.push({
|
|
900
1277
|
type: "text-delta",
|
|
901
1278
|
id: parsed.id,
|
|
902
|
-
delta: parsed.delta
|
|
1279
|
+
delta: parsed.delta,
|
|
1280
|
+
...parsed.providerMetadata ? {
|
|
1281
|
+
providerMetadata: this.partMetadata(
|
|
1282
|
+
parsed.providerMetadata,
|
|
1283
|
+
id
|
|
1284
|
+
)
|
|
1285
|
+
} : {}
|
|
903
1286
|
});
|
|
904
1287
|
break;
|
|
905
1288
|
}
|
|
906
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
|
+
}
|
|
907
1308
|
parts.push({
|
|
908
1309
|
type: "text-end",
|
|
909
|
-
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 } : {}
|
|
910
1335
|
});
|
|
911
1336
|
break;
|
|
912
1337
|
}
|
|
@@ -917,7 +1342,8 @@ var BunnyAgentLanguageModel = class {
|
|
|
917
1342
|
id: parsed.toolCallId,
|
|
918
1343
|
toolName: parsed.toolName,
|
|
919
1344
|
dynamic: readToolDynamicFlag(parsed),
|
|
920
|
-
providerExecuted: parsed.providerExecuted
|
|
1345
|
+
providerExecuted: parsed.providerExecuted,
|
|
1346
|
+
providerMetadata: this.toolPartMetadata(parsed)
|
|
921
1347
|
});
|
|
922
1348
|
break;
|
|
923
1349
|
}
|
|
@@ -925,7 +1351,16 @@ var BunnyAgentLanguageModel = class {
|
|
|
925
1351
|
parts.push({
|
|
926
1352
|
type: "tool-input-delta",
|
|
927
1353
|
id: parsed.toolCallId,
|
|
928
|
-
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)
|
|
929
1364
|
});
|
|
930
1365
|
break;
|
|
931
1366
|
}
|
|
@@ -940,11 +1375,14 @@ var BunnyAgentLanguageModel = class {
|
|
|
940
1375
|
toolName,
|
|
941
1376
|
input: JSON.stringify(input),
|
|
942
1377
|
dynamic: readToolDynamicFlag(parsed),
|
|
943
|
-
providerExecuted: parsed.providerExecuted
|
|
1378
|
+
providerExecuted: parsed.providerExecuted,
|
|
1379
|
+
providerMetadata: this.toolPartMetadata(parsed)
|
|
944
1380
|
});
|
|
945
1381
|
break;
|
|
946
1382
|
}
|
|
947
1383
|
case "tool-output-available": {
|
|
1384
|
+
if (this.completedToolIds.has(parsed.toolCallId)) break;
|
|
1385
|
+
this.completedToolIds.add(parsed.toolCallId);
|
|
948
1386
|
const toolName = this.toolNameMap.get(parsed.toolCallId);
|
|
949
1387
|
parts.push({
|
|
950
1388
|
type: "tool-result",
|
|
@@ -952,11 +1390,14 @@ var BunnyAgentLanguageModel = class {
|
|
|
952
1390
|
toolName: toolName ?? "",
|
|
953
1391
|
result: parsed.output,
|
|
954
1392
|
isError: parsed.isError,
|
|
955
|
-
dynamic: readToolDynamicFlag(parsed)
|
|
1393
|
+
dynamic: readToolDynamicFlag(parsed),
|
|
1394
|
+
providerMetadata: this.toolPartMetadata(parsed)
|
|
956
1395
|
});
|
|
957
1396
|
break;
|
|
958
1397
|
}
|
|
959
1398
|
case "tool-output-error": {
|
|
1399
|
+
if (this.completedToolIds.has(parsed.toolCallId)) break;
|
|
1400
|
+
this.completedToolIds.add(parsed.toolCallId);
|
|
960
1401
|
const toolName = this.toolNameMap.get(parsed.toolCallId);
|
|
961
1402
|
parts.push({
|
|
962
1403
|
type: "tool-result",
|
|
@@ -964,17 +1405,18 @@ var BunnyAgentLanguageModel = class {
|
|
|
964
1405
|
toolName: toolName ?? "",
|
|
965
1406
|
result: parsed.errorText ?? "Tool execution failed",
|
|
966
1407
|
isError: true,
|
|
967
|
-
dynamic: readToolDynamicFlag(parsed)
|
|
1408
|
+
dynamic: readToolDynamicFlag(parsed),
|
|
1409
|
+
providerMetadata: this.toolPartMetadata(parsed)
|
|
968
1410
|
});
|
|
969
1411
|
break;
|
|
970
1412
|
}
|
|
971
1413
|
case "error": {
|
|
972
|
-
|
|
1414
|
+
const message = parsed.errorText || parsed.error || "Unknown stream error";
|
|
1415
|
+
if (this.seenStreamErrors.has(message)) break;
|
|
1416
|
+
this.seenStreamErrors.add(message);
|
|
973
1417
|
parts.push({
|
|
974
1418
|
type: "error",
|
|
975
|
-
error: new Error(
|
|
976
|
-
parsed.errorText || parsed.error || "Unknown stream error"
|
|
977
|
-
)
|
|
1419
|
+
error: new Error(message)
|
|
978
1420
|
});
|
|
979
1421
|
break;
|
|
980
1422
|
}
|
|
@@ -988,6 +1430,10 @@ var BunnyAgentLanguageModel = class {
|
|
|
988
1430
|
} else {
|
|
989
1431
|
finishReason = this.mapFinishReason(rawFinishReason);
|
|
990
1432
|
}
|
|
1433
|
+
if (this.requestedSearchContractVersion === 1 && this.acknowledgedSearchContract === void 0 && finishReason.unified !== "error") {
|
|
1434
|
+
parts.push(...this.buildMissingAcknowledgementErrorParts());
|
|
1435
|
+
break;
|
|
1436
|
+
}
|
|
991
1437
|
const usage = normalizeBunnyAgentUsage(
|
|
992
1438
|
parsed.messageMetadata ?? void 0
|
|
993
1439
|
) ?? createEmptyUsage();
|
|
@@ -995,12 +1441,14 @@ var BunnyAgentLanguageModel = class {
|
|
|
995
1441
|
type: "finish",
|
|
996
1442
|
finishReason,
|
|
997
1443
|
usage,
|
|
998
|
-
providerMetadata:
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1444
|
+
providerMetadata: this.partMetadata(
|
|
1445
|
+
mergeProviderMetadata(
|
|
1446
|
+
parsed.providerMetadata,
|
|
1447
|
+
{
|
|
1448
|
+
"bunny-agent": parsed.messageMetadata ?? {}
|
|
1449
|
+
}
|
|
1450
|
+
)
|
|
1451
|
+
)
|
|
1004
1452
|
});
|
|
1005
1453
|
break;
|
|
1006
1454
|
}
|
|
@@ -1096,27 +1544,31 @@ function createBunnyAgent(defaultOptions) {
|
|
|
1096
1544
|
const logger = getProviderLogger(defaultOptions);
|
|
1097
1545
|
if (!defaultOptions.sandbox) {
|
|
1098
1546
|
throw new Error(
|
|
1099
|
-
"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."
|
|
1100
1548
|
);
|
|
1101
1549
|
}
|
|
1102
1550
|
const createModel = (modelId, options = {}) => {
|
|
1103
1551
|
const mergedSkillPaths = options.skillPaths !== void 0 ? options.skillPaths : defaultOptions.skillPaths;
|
|
1104
1552
|
const mergedAllowedTools = options.allowedTools !== void 0 ? options.allowedTools : defaultOptions.allowedTools;
|
|
1553
|
+
const searchContractVersion = options.searchContractVersion ?? defaultOptions.searchContractVersion;
|
|
1105
1554
|
const runner = {
|
|
1106
1555
|
model: modelId,
|
|
1107
1556
|
runnerType: options.runnerType ?? defaultOptions.runnerType,
|
|
1108
1557
|
outputFormat: "stream",
|
|
1109
1558
|
maxTurns: options.maxTurns ?? defaultOptions.maxTurns,
|
|
1559
|
+
...(options.yolo ?? defaultOptions.yolo) !== void 0 ? { yolo: options.yolo ?? defaultOptions.yolo } : {},
|
|
1110
1560
|
...options.systemPrompt ?? defaultOptions.systemPrompt ? { systemPrompt: options.systemPrompt ?? defaultOptions.systemPrompt } : {},
|
|
1111
1561
|
...mergedSkillPaths && mergedSkillPaths.length > 0 ? { skillPaths: mergedSkillPaths } : {},
|
|
1112
1562
|
...mergedAllowedTools !== void 0 ? { allowedTools: mergedAllowedTools } : {},
|
|
1113
1563
|
...options.effort ?? defaultOptions.effort ? {
|
|
1114
1564
|
effort: options.effort ?? defaultOptions.effort
|
|
1115
|
-
} : {}
|
|
1565
|
+
} : {},
|
|
1566
|
+
...searchContractVersion === 1 ? { searchContractVersion: 1 } : {}
|
|
1116
1567
|
};
|
|
1117
1568
|
const mergedOptions = {
|
|
1118
1569
|
...defaultOptions,
|
|
1119
1570
|
...options,
|
|
1571
|
+
searchContractVersion,
|
|
1120
1572
|
runner,
|
|
1121
1573
|
env: {
|
|
1122
1574
|
...defaultOptions.env,
|