@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/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 = {
@@ -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
- resetStreamState() {
605
- this.sessionId = void 0;
606
- this.toolNameMap = /* @__PURE__ */ new Map();
607
- this.legacyTextPartCounter = 0;
608
- this.receivedFinish = false;
609
- 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
+ ];
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
- const text = new TextDecoder().decode(value);
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.receivedFinish = true;
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
- { type: "text-end", id }
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
- 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) {
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
- 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);
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
- "bunny-agent": {
994
- ...parsed.messageMetadata ?? {},
995
- sessionId: this.sessionId
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,