@cydm/pie 2.6.5 → 2.6.6

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.
@@ -722,6 +722,9 @@ var NodeFileSystem = class {
722
722
  }
723
723
  }
724
724
  async readTextRange(path, options, signal) {
725
+ if (options?.tailLines !== void 0) {
726
+ return this.readTextRangeTail(path, options, signal);
727
+ }
725
728
  const fs = getNodeFs();
726
729
  const range = normalizeRangeOptions(options);
727
730
  const stat = await fs.promises.stat(path);
@@ -747,6 +750,7 @@ var NodeFileSystem = class {
747
750
  const selectedBinaryDetector = new StreamingBinaryDetector();
748
751
  const textDecoder = new TextDecoder("utf-8", { ignoreBOM: true });
749
752
  let rawLineIndex = 0;
753
+ let lastCharWasNewline = false;
750
754
  const selectedEndLine = Number.isFinite(range.limitLines) ? range.offsetLine + range.limitLines : Number.POSITIVE_INFINITY;
751
755
  const startLine = () => {
752
756
  if (lineStarted) return;
@@ -843,6 +847,7 @@ var NodeFileSystem = class {
843
847
  for (const char of text) {
844
848
  if (char === "\n") finishLine();
845
849
  else scanLineChar(char);
850
+ lastCharWasNewline = char === "\n";
846
851
  }
847
852
  };
848
853
  const stream = fs.createReadStream(path);
@@ -877,6 +882,181 @@ var NodeFileSystem = class {
877
882
  firstLineExceedsLimit,
878
883
  selectedBytesLookBinary: selectedBinaryDetector.finish(),
879
884
  fileBytesLookBinary: fileBinaryDetector.finish(),
885
+ endsWithNewline: lastCharWasNewline,
886
+ mtimeMs: stat.mtime.getTime(),
887
+ size: stat.size
888
+ };
889
+ }
890
+ /**
891
+ * Tail read: select the last N lines in ONE forward scan. The full-file
892
+ * hash and totalLines (which the observation ledger requires of every
893
+ * read) force a complete scan anyway, so the tail window is extracted with
894
+ * a bounded ring buffer during that same scan — no probing passes, no
895
+ * second/third read. Ring bounds: at most `effectiveN` line entries and
896
+ * 2×maxOutputBytes of text; a single line longer than maxOutputBytes keeps
897
+ * only its byte length (it can never be output). When the ring evicts
898
+ * older lines (window bytes over budget), the result degrades honestly:
899
+ * truncated=true, and the reported window covers exactly what was kept.
900
+ */
901
+ async readTextRangeTail(path, options, signal) {
902
+ const fs = getNodeFs();
903
+ const stat = await fs.promises.stat(path);
904
+ const maxOutputLines = Number.isFinite(options.maxOutputLines) ? Math.max(0, Math.floor(options.maxOutputLines)) : Number.POSITIVE_INFINITY;
905
+ const maxOutputBytes = Number.isFinite(options.maxOutputBytes) ? Math.max(0, Math.floor(options.maxOutputBytes)) : Number.POSITIVE_INFINITY;
906
+ const requested = Math.max(1, Math.floor(options.tailLines));
907
+ const effectiveN = Math.max(1, Math.min(requested, maxOutputLines));
908
+ const fullHasher = createTextHasher();
909
+ const fileBinaryDetector = new StreamingBinaryDetector();
910
+ const textDecoder = new TextDecoder("utf-8", { ignoreBOM: true });
911
+ const entries = [];
912
+ let entryBytes = 0;
913
+ let totalLines = 0;
914
+ let lastCharWasNewline = false;
915
+ let currentLine = "";
916
+ let currentLineBytes = 0;
917
+ let keepLineText = true;
918
+ const ringByteBudget = Number.isFinite(maxOutputBytes) ? 2 * maxOutputBytes : Number.POSITIVE_INFINITY;
919
+ const trimRing = () => {
920
+ while (entries.length > effectiveN || entries.length > 1 && entryBytes > ringByteBudget) {
921
+ const evicted = entries.shift();
922
+ entryBytes -= evicted.bytes;
923
+ }
924
+ };
925
+ const scanText = (text) => {
926
+ if (!text) return;
927
+ fullHasher.update(text);
928
+ for (const char of text) {
929
+ if (char === "\n") {
930
+ entries.push({ text: keepLineText ? currentLine : null, bytes: currentLineBytes });
931
+ entryBytes += currentLineBytes;
932
+ totalLines++;
933
+ currentLine = "";
934
+ currentLineBytes = 0;
935
+ keepLineText = true;
936
+ trimRing();
937
+ } else {
938
+ const charBytes = byteLength(char);
939
+ currentLineBytes += charBytes;
940
+ if (currentLineBytes > maxOutputBytes) {
941
+ keepLineText = false;
942
+ currentLine = "";
943
+ } else {
944
+ currentLine += char;
945
+ }
946
+ }
947
+ lastCharWasNewline = char === "\n";
948
+ }
949
+ };
950
+ const stream = fs.createReadStream(path);
951
+ try {
952
+ for await (const chunk of stream) {
953
+ if (signal?.aborted) {
954
+ stream.destroy();
955
+ throw new Error("Operation aborted");
956
+ }
957
+ const bytes = chunk;
958
+ fileBinaryDetector.update(bytes);
959
+ scanText(textDecoder.decode(bytes, { stream: true }));
960
+ }
961
+ scanText(textDecoder.decode());
962
+ } finally {
963
+ stream.destroy();
964
+ }
965
+ if (lastCharWasNewline) {
966
+ totalLines++;
967
+ } else if (currentLine !== "" || currentLineBytes > 0) {
968
+ entries.push({ text: keepLineText ? currentLine : null, bytes: currentLineBytes });
969
+ entryBytes += currentLineBytes;
970
+ totalLines++;
971
+ trimRing();
972
+ } else if (totalLines === 0) {
973
+ totalLines = 1;
974
+ }
975
+ const phantomIncluded = lastCharWasNewline;
976
+ const emptyFileSingleLine = entries.length === 0 && !phantomIncluded && totalLines === 1;
977
+ const windowLines = entries.length + (phantomIncluded ? 1 : 0) + (emptyFileSingleLine ? 1 : 0);
978
+ const selectedBytes = entries.reduce((sum, entry) => sum + entry.bytes, 0) + Math.max(0, windowLines - 1);
979
+ const firstLineBytes = entries.length > 0 ? entries[0].bytes : 0;
980
+ const firstLineExceedsLimit = entries.length === 1 && entries[0].text === null;
981
+ const streamedLines = totalLines - (phantomIncluded ? 1 : 0) - (emptyFileSingleLine ? 1 : 0);
982
+ const lostWindowLines = Math.max(0, Math.min(streamedLines, effectiveN) - entries.length);
983
+ const outputLines = [];
984
+ let outputBytes = 0;
985
+ let truncated = lostWindowLines > 0;
986
+ let truncatedBy = lostWindowLines > 0 ? "bytes" : null;
987
+ let firstShownEntryIndex;
988
+ const phantomReserve = phantomIncluded && !emptyFileSingleLine ? 1 : 0;
989
+ for (let index = entries.length - 1; index >= 0; index--) {
990
+ const entry = entries[index];
991
+ if (outputLines.length + phantomReserve >= maxOutputLines) {
992
+ truncated = true;
993
+ truncatedBy = truncatedBy ?? "lines";
994
+ break;
995
+ }
996
+ if (entry.text === null) {
997
+ truncated = true;
998
+ truncatedBy = "bytes";
999
+ break;
1000
+ }
1001
+ const candidate = outputBytes + (outputLines.length > 0 ? 1 : 0) + entry.bytes;
1002
+ if (candidate > maxOutputBytes) {
1003
+ truncated = true;
1004
+ truncatedBy = "bytes";
1005
+ break;
1006
+ }
1007
+ outputLines.unshift(entry.text);
1008
+ outputBytes = candidate;
1009
+ firstShownEntryIndex = index;
1010
+ }
1011
+ if (phantomIncluded) {
1012
+ if (outputLines.length + 1 <= maxOutputLines || outputLines.length === 0) {
1013
+ const phantomBytes = outputLines.length > 0 ? 1 : 0;
1014
+ if (outputBytes + phantomBytes <= maxOutputBytes) {
1015
+ outputLines.push("");
1016
+ outputBytes += phantomBytes;
1017
+ } else {
1018
+ truncated = true;
1019
+ truncatedBy = "bytes";
1020
+ }
1021
+ } else {
1022
+ truncated = true;
1023
+ truncatedBy = truncatedBy ?? "lines";
1024
+ }
1025
+ }
1026
+ if (emptyFileSingleLine) {
1027
+ outputLines.push("");
1028
+ }
1029
+ if (firstLineExceedsLimit) {
1030
+ truncated = true;
1031
+ truncatedBy = "bytes";
1032
+ }
1033
+ const realLastLine = totalLines - (phantomIncluded ? 1 : 0);
1034
+ const outputStartLine = outputLines.length === 0 ? totalLines + 1 : firstShownEntryIndex !== void 0 ? realLastLine - entries.length + 1 + firstShownEntryIndex : totalLines;
1035
+ const content = outputLines.join("\n");
1036
+ const selectedHasher = createTextHasher();
1037
+ selectedHasher.update(content);
1038
+ const selectedBinaryDetector = new StreamingBinaryDetector();
1039
+ if (content.length > 0) {
1040
+ selectedBinaryDetector.update(Buffer.from(content, "utf-8"));
1041
+ }
1042
+ return {
1043
+ content,
1044
+ contentHash: fullHasher.digest(),
1045
+ selectedContentHash: selectedHasher.digest(),
1046
+ totalLines,
1047
+ totalBytes: stat.size,
1048
+ selectedLines: windowLines,
1049
+ selectedBytes,
1050
+ firstLineBytes,
1051
+ outputLines: outputLines.length,
1052
+ outputBytes,
1053
+ truncated,
1054
+ truncatedBy,
1055
+ firstLineExceedsLimit,
1056
+ selectedBytesLookBinary: selectedBinaryDetector.finish(),
1057
+ fileBytesLookBinary: fileBinaryDetector.finish(),
1058
+ endsWithNewline: lastCharWasNewline,
1059
+ outputStartLine,
880
1060
  mtimeMs: stat.mtime.getTime(),
881
1061
  size: stat.size
882
1062
  };
@@ -1206,6 +1386,9 @@ var PuerTSFileSystem = class {
1206
1386
  if (signal?.aborted) {
1207
1387
  throw new Error("Operation aborted");
1208
1388
  }
1389
+ if (options?.tailLines !== void 0 && Number.isFinite(options.tailLines) && options.tailLines > 0) {
1390
+ return this.readTextRangeTailViaBridge(path, options, signal);
1391
+ }
1209
1392
  const bridge = getPieFileBridge();
1210
1393
  const normalizedPath = this.normalizePath(path);
1211
1394
  if (typeof bridge.StartReadTextRange !== "function" || typeof bridge.CancelRequest !== "function" || typeof bridge.ReleaseRequest !== "function") {
@@ -1237,6 +1420,71 @@ var PuerTSFileSystem = class {
1237
1420
  bridge.ReleaseRequest(requestId);
1238
1421
  }
1239
1422
  }
1423
+ /**
1424
+ * Tail fallback for the PuerTS host: the Unity C# bridge has no native
1425
+ * tail support yet (follow-up: a purpose-built bridge method mirroring
1426
+ * NodeFileSystem's single-scan ring buffer). Until then, translate
1427
+ * tailLines into the bridge's offset/limit windows — a 1-line probe for
1428
+ * the line count, one for phantom detection, then the window read. The
1429
+ * extra scans stay inside the platform so every caller (the shared read
1430
+ * tool, future hosts) sees one uniform tailLines contract.
1431
+ *
1432
+ * Two contract guards live here because the bridge reads windows FORWARD:
1433
+ * 1. The C# side keeps the window's HEAD under the byte cap — the opposite
1434
+ * of tail semantics. Halve the window until it fits so the delivered
1435
+ * content always ends at the file's last line (matching NodeFileSystem);
1436
+ * a single over-cap line degrades to firstLineExceedsLimit, the same
1437
+ * signal Node produces.
1438
+ * 2. Out-of-band appends between the probes and the window read would
1439
+ * stale the line count; verify mtime stability and retry a bounded
1440
+ * number of times, then fall through best-effort (append-heavy logs
1441
+ * legitimately churn).
1442
+ */
1443
+ async readTextRangeTailViaBridge(path, options, signal) {
1444
+ const probeOptions = {
1445
+ offsetLine: 0,
1446
+ limitLines: 1,
1447
+ maxOutputLines: 1,
1448
+ maxOutputBytes: options.maxOutputBytes
1449
+ };
1450
+ const maxOutputLines = Number.isFinite(options.maxOutputLines) ? Math.floor(options.maxOutputLines) : Number.POSITIVE_INFINITY;
1451
+ for (let attempt = 0; ; attempt++) {
1452
+ const countProbe = await this.readTextRange(path, probeOptions, signal);
1453
+ const totalLines = countProbe.totalLines;
1454
+ const requested = Math.floor(options.tailLines);
1455
+ let effectiveN = Math.max(1, Math.min(requested, maxOutputLines));
1456
+ const lastProbe = await this.readTextRange(path, { ...probeOptions, offsetLine: Math.max(0, totalLines - 1) }, signal);
1457
+ const phantom = lastProbe.selectedBytes === 0 && !lastProbe.firstLineExceedsLimit && totalLines > 1;
1458
+ let result;
1459
+ let windowOffsetLine = 0;
1460
+ for (; ; ) {
1461
+ windowOffsetLine = Math.max(0, totalLines - effectiveN - (phantom ? 1 : 0));
1462
+ result = await this.readTextRange(path, {
1463
+ offsetLine: windowOffsetLine,
1464
+ limitLines: effectiveN + (phantom ? 1 : 0),
1465
+ maxOutputLines: options.maxOutputLines,
1466
+ maxOutputBytes: options.maxOutputBytes
1467
+ }, signal);
1468
+ const byteTruncated = result.truncated && result.truncatedBy === "bytes" && !result.firstLineExceedsLimit;
1469
+ const oversizedHead = result.truncated && result.firstLineExceedsLimit;
1470
+ if (byteTruncated && effectiveN > 1) {
1471
+ effectiveN = Math.max(1, Math.floor(effectiveN / 2));
1472
+ continue;
1473
+ }
1474
+ if (oversizedHead) {
1475
+ const nextEffectiveN = totalLines - (phantom ? 1 : 0) - (windowOffsetLine + 1);
1476
+ if (nextEffectiveN < 1 || nextEffectiveN >= effectiveN) break;
1477
+ effectiveN = nextEffectiveN;
1478
+ continue;
1479
+ }
1480
+ break;
1481
+ }
1482
+ const stable = countProbe.mtimeMs === lastProbe.mtimeMs && lastProbe.mtimeMs === result.mtimeMs && countProbe.contentHash === lastProbe.contentHash && lastProbe.contentHash === result.contentHash;
1483
+ if (stable || attempt >= 2) {
1484
+ return { ...result, endsWithNewline: phantom, outputStartLine: windowOffsetLine + 1 };
1485
+ }
1486
+ }
1487
+ }
1240
1488
  async writeFile(path, data, encoding = "utf-8") {
1241
1489
  const bridge = getPieFileBridge();
1242
1490
  const normalizedPath = this.normalizePath(path);
@@ -2750,6 +2998,17 @@ function isPuertsReparsePoint(path) {
2750
2998
  return false;
2751
2999
  }
2752
3000
  }
3001
+ var warnedPuertsDeletionShells = false;
3002
+ function warnPuertsDeletionShellsOnce(path) {
3003
+ if (warnedPuertsDeletionShells) return;
3004
+ warnedPuertsDeletionShells = true;
3005
+ try {
3006
+ console.warn(
3007
+ `[platform] Directory.Delete is unbound on this host; '${path}' contents were removed but empty directory shells cannot be deleted by this binding (logged once)`
3008
+ );
3009
+ } catch {
3010
+ }
3011
+ }
2753
3012
  function deletePuertsDirectoryByEnumeration(path, recursive) {
2754
3013
  const directory = CS.System.IO.Directory;
2755
3014
  const file = CS.System.IO.File;
@@ -2764,13 +3023,19 @@ function deletePuertsDirectoryByEnumeration(path, recursive) {
2764
3023
  );
2765
3024
  }
2766
3025
  const files = Array.from(directory.GetFiles(path) ?? []);
2767
- for (const entry of files) file.Delete(entry);
2768
- if (!recursive) return;
2769
3026
  const subdirs = Array.from(directory.GetDirectories(path) ?? []);
3027
+ if (!recursive) {
3028
+ if (files.length > 0 || subdirs.length > 0) {
3029
+ throw new Error(`Directory not empty: ${path}`);
3030
+ }
3031
+ return;
3032
+ }
3033
+ for (const entry of files) file.Delete(entry);
2770
3034
  for (const subdir of subdirs) {
2771
3035
  if (isPuertsReparsePoint(String(subdir))) continue;
2772
3036
  deletePuertsDirectoryByEnumeration(String(subdir), true);
2773
3037
  }
3038
+ warnPuertsDeletionShellsOnce(path);
2774
3039
  }
2775
3040
 
2776
3041
  // ../../packages/platform/src/index.ts
@@ -2841,10 +3106,15 @@ function parseStreamingJson(partialJson) {
2841
3106
  }
2842
3107
 
2843
3108
  // ../../packages/ai/src/providers/simple-options.ts
3109
+ var DEFAULT_MAX_TOKENS = 32e3;
3110
+ function positiveFiniteOrInfinity(value) {
3111
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : Number.POSITIVE_INFINITY;
3112
+ }
2844
3113
  function buildBaseOptions(model, options, apiKey) {
3114
+ const modelMaxTokens = positiveFiniteOrInfinity(model.maxTokens);
2845
3115
  return {
2846
3116
  temperature: options?.temperature,
2847
- maxTokens: options?.maxTokens || Math.min(model.maxTokens, 32e3),
3117
+ maxTokens: options?.maxTokens || Math.min(modelMaxTokens, DEFAULT_MAX_TOKENS),
2848
3118
  signal: options?.signal,
2849
3119
  apiKey: apiKey || options?.apiKey,
2850
3120
  cacheRetention: options?.cacheRetention,
@@ -2866,11 +3136,18 @@ function adjustMaxTokensForThinking(baseMaxTokens, modelMaxTokens, reasoningLeve
2866
3136
  medium: 8192,
2867
3137
  high: 16384
2868
3138
  };
2869
- const budgets = { ...defaultBudgets, ...customBudgets };
3139
+ const budgets = { ...defaultBudgets };
3140
+ if (customBudgets) {
3141
+ for (const key of Object.keys(defaultBudgets)) {
3142
+ const value = customBudgets[key];
3143
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) budgets[key] = value;
3144
+ }
3145
+ }
2870
3146
  const minOutputTokens = 1024;
2871
3147
  const level = clampReasoning(reasoningLevel);
2872
- let thinkingBudget = budgets[level];
2873
- const maxTokens = Math.min(baseMaxTokens + thinkingBudget, modelMaxTokens);
3148
+ let thinkingBudget = budgets[level] ?? defaultBudgets[level] ?? 0;
3149
+ const outputCap = positiveFiniteOrInfinity(modelMaxTokens);
3150
+ const maxTokens = Math.min(baseMaxTokens + thinkingBudget, outputCap);
2874
3151
  if (maxTokens <= thinkingBudget) {
2875
3152
  thinkingBudget = Math.max(0, maxTokens - minOutputTokens);
2876
3153
  }
@@ -3796,7 +4073,11 @@ var streamOpenAICompletions = (model, context, options) => {
3796
4073
  }
3797
4074
  const emittedCallCounts = /* @__PURE__ */ new Map();
3798
4075
  const excessDuplicateEntries = /* @__PURE__ */ new Set();
4076
+ const repeatExempt = new Set(
4077
+ (context.tools ?? []).filter((tool) => tool.allowIdenticalRepeatCalls === true).map((tool) => tool.name)
4078
+ );
3799
4079
  for (const entry of toolCallEntries) {
4080
+ if (repeatExempt.has(entry.block.name)) continue;
3800
4081
  const key = `${entry.block.name}
3801
4082
  ${entry.partialArgs}`;
3802
4083
  const seen = (emittedCallCounts.get(key) ?? 0) + 1;
@@ -3871,8 +4152,18 @@ var streamSimpleOpenAICompletions = (model, context, options) => {
3871
4152
  }
3872
4153
  const base = buildBaseOptions(model, options, apiKey);
3873
4154
  const reasoningEffort = clampReasoning(options?.reasoning);
4155
+ const needsThinkingAllowance = Boolean(model.reasoning) && options?.reasoning !== void 0;
4156
+ const adjusted = needsThinkingAllowance ? adjustMaxTokensForThinking(base.maxTokens || 0, model.maxTokens, options.reasoning, options?.thinkingBudgets) : void 0;
4157
+ let maxTokens = adjusted ? adjusted.maxTokens : base.maxTokens;
4158
+ if (maxTokens === void 0) {
4159
+ maxTokens = DEFAULT_MAX_TOKENS;
4160
+ }
4161
+ if (options?.maxTokens === void 0) {
4162
+ maxTokens = Math.min(maxTokens, DEFAULT_MAX_TOKENS);
4163
+ }
3874
4164
  return streamOpenAICompletions(model, context, {
3875
4165
  ...base,
4166
+ maxTokens,
3876
4167
  reasoningEffort
3877
4168
  });
3878
4169
  };
@@ -7793,6 +8084,7 @@ export {
7793
8084
  type_exports,
7794
8085
  ExtendsUndefinedCheck,
7795
8086
  EventStream,
8087
+ AssistantMessageEventStream,
7796
8088
  getModelCacheCapability,
7797
8089
  EMPTY_ASSISTANT_CONTENT_ERROR,
7798
8090
  OPENAI_COMPAT_PROTOCOL_ERROR_CODE,