@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/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
- var BunnyAgentLanguageModel = class {
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, { text: "" });
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
- type: "tool-call",
482
- toolCallId: value.toolCallId,
483
- toolName: value.toolName,
484
- input: value.input,
485
- providerExecuted: value.providerExecuted
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
- resetStreamState() {
610
- this.sessionId = void 0;
611
- this.toolNameMap = /* @__PURE__ */ new Map();
612
- this.legacyTextPartCounter = 0;
613
- this.receivedFinish = false;
614
- this.activeReasoningPartId = void 0;
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.receivedFinish = true;
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
- { type: "text-end", id }
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
- if (metadata?.sessionId && typeof metadata.sessionId === "string") {
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
- this.receivedFinish = true;
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
- "bunny-agent": {
1000
- ...parsed.messageMetadata ?? {},
1001
- sessionId: this.sessionId
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,