@apo-ai/sdk 0.9.2 → 0.9.4

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.
@@ -377,6 +377,9 @@ function levenshtein(a, b) {
377
377
  return prev[n];
378
378
  }
379
379
 
380
+ // src/agent-task/checks/judge.ts
381
+ import { createHash } from "crypto";
382
+
380
383
  // src/agent-task/checks/second-judge.ts
381
384
  var SECOND_JUDGE_TIMEOUT_MS = 3e4;
382
385
  function resolveSecondJudgeModel() {
@@ -557,8 +560,19 @@ function parseJudgeJson(raw) {
557
560
  reasoning: "Judge response could not be parsed as JSON \u2014 the verdict is unknown, so this check is treated as a failure. The model's raw response is available in the judge metadata."
558
561
  };
559
562
  }
560
- var JUDGE_TIMEOUT_MS = 3e5;
561
563
  var JUDGE_IDLE_TIMEOUT_MS = 9e4;
564
+ function envMs(name, fallback) {
565
+ const raw = process.env[name]?.trim();
566
+ if (!raw) return fallback;
567
+ const ms = Number(raw);
568
+ return Number.isFinite(ms) && ms > 0 ? ms : fallback;
569
+ }
570
+ function judgeFirstDataTimeoutMs() {
571
+ return envMs("APO_JUDGE_TIMEOUT_MS", 3e5);
572
+ }
573
+ function judgeMaxDurationMs() {
574
+ return envMs("APO_JUDGE_MAX_DURATION_MS", 12e5);
575
+ }
562
576
  var JUDGE_RETRY_DELAY_MS = 1e3;
563
577
  var JUDGE_RETRY_AFTER_CAP_MS = 2e4;
564
578
  var JUDGE_MIN_RETRY_BUDGET_MS = 6e4;
@@ -644,6 +658,36 @@ async function readCompletion(response, onData) {
644
658
  return { text, usage, finishReason };
645
659
  }
646
660
  var prefixQueues = /* @__PURE__ */ new Map();
661
+ function promptCacheKey(prefix) {
662
+ return `apo-${createHash("sha256").update(prefix).digest("hex").slice(0, 32)}`;
663
+ }
664
+ var cacheKeyRejectedBy = /* @__PURE__ */ new Set();
665
+ var CACHE_KEY_REJECTED = /(unknown|unrecogni[sz]ed|unsupported|unexpected|extra|not permitted|not allowed|cannot find|invalid)[^\n]{0,80}prompt_cache_key|prompt_cache_key[^\n]{0,80}(unknown|unrecogni[sz]ed|unsupported|unexpected|not permitted|not allowed|not supported|extra)/i;
666
+ function rejectsPromptCacheKey(status, body) {
667
+ return status === 400 && CACHE_KEY_REJECTED.test(body);
668
+ }
669
+ async function fetchWithPromptCacheKey(input, init) {
670
+ let body;
671
+ try {
672
+ body = typeof init?.body === "string" ? JSON.parse(init.body) : void 0;
673
+ } catch {
674
+ body = void 0;
675
+ }
676
+ if (!body || !("prompt_cache_key" in body)) return fetch(input, init);
677
+ const scope = `${input instanceof Request ? input.url : String(input)}\0${String(body.model)}`;
678
+ const { prompt_cache_key: _omitted, ...rest } = body;
679
+ const withoutKey = () => fetch(input, { ...init, body: JSON.stringify(rest) });
680
+ if (cacheKeyRejectedBy.has(scope)) return withoutKey();
681
+ const response = await fetch(input, init);
682
+ if (response.status !== 400) return response;
683
+ const text = await response.text();
684
+ if (!rejectsPromptCacheKey(response.status, text)) {
685
+ return new Response(text, { status: response.status, statusText: response.statusText, headers: response.headers });
686
+ }
687
+ const retry = await withoutKey();
688
+ if (retry.ok) cacheKeyRejectedBy.add(scope);
689
+ return retry;
690
+ }
647
691
  function runWithSharedPrefix(key, task2) {
648
692
  const prev = prefixQueues.get(key) ?? Promise.resolve();
649
693
  const next = prev.then(task2, task2);
@@ -684,6 +728,7 @@ ${instructionText}`,
684
728
  const cacheKey = `${args.model}\0${briefingText}\0${deliverableText}`;
685
729
  const requestBody = (stream) => JSON.stringify({
686
730
  model: args.model,
731
+ prompt_cache_key: promptCacheKey(cacheKey),
687
732
  messages: [
688
733
  {
689
734
  role: "system",
@@ -702,23 +747,36 @@ ${instructionText}`,
702
747
  response_format: { type: "json_object" },
703
748
  ...stream ? { stream: true, stream_options: { include_usage: true } } : {}
704
749
  });
705
- const attempt = async (stream, deadline) => {
750
+ const attempt = async (stream, maxDeadline) => {
706
751
  const controller = new AbortController();
707
752
  const expire = (reason, name) => () => controller.abort(new DOMException(reason, name));
708
- const total = setTimeout(
709
- expire(`no complete response within ${JUDGE_TIMEOUT_MS / 1e3}s`, "TimeoutError"),
710
- Math.max(deadline - Date.now(), 0)
753
+ const firstDataMs = judgeFirstDataTimeoutMs();
754
+ const runaway = setTimeout(
755
+ expire(
756
+ `still streaming after ${Math.round(judgeMaxDurationMs() / 1e3)}s (APO_JUDGE_MAX_DURATION_MS)`,
757
+ "TimeoutError"
758
+ ),
759
+ Math.max(maxDeadline - Date.now(), 0)
760
+ );
761
+ let waiting = setTimeout(
762
+ expire(
763
+ `no data within ${Math.round(firstDataMs / 1e3)}s (APO_JUDGE_TIMEOUT_MS)`,
764
+ "FirstDataTimeoutError"
765
+ ),
766
+ firstDataMs
711
767
  );
712
768
  const stalled = expire(`no data for ${JUDGE_IDLE_TIMEOUT_MS / 1e3}s`, "IdleTimeoutError");
713
769
  let idle;
714
770
  const onData = () => {
771
+ clearTimeout(waiting);
772
+ waiting = void 0;
715
773
  clearTimeout(idle);
716
774
  idle = setTimeout(stalled, JUDGE_IDLE_TIMEOUT_MS);
717
775
  };
718
776
  try {
719
777
  let response;
720
778
  try {
721
- response = await fetch(`${baseURL}/chat/completions`, {
779
+ response = await fetchWithPromptCacheKey(`${baseURL}/chat/completions`, {
722
780
  method: "POST",
723
781
  signal: controller.signal,
724
782
  headers: {
@@ -748,7 +806,8 @@ ${instructionText}`,
748
806
  return transportFailure("Judge response failed", controller.signal.reason ?? error);
749
807
  }
750
808
  } finally {
751
- clearTimeout(total);
809
+ clearTimeout(runaway);
810
+ clearTimeout(waiting);
752
811
  clearTimeout(idle);
753
812
  controller.abort();
754
813
  }
@@ -768,7 +827,7 @@ ${instructionText}`,
768
827
  };
769
828
  return runWithSharedPrefix(cacheKey, async () => {
770
829
  const startedAt = Date.now();
771
- const deadline = startedAt + JUDGE_TIMEOUT_MS;
830
+ const deadline = startedAt + judgeMaxDurationMs();
772
831
  let stream = true;
773
832
  let result = await attempt(stream, deadline);
774
833
  const truncated = result.kind === "completion" && isTruncated(result.completion);
@@ -860,7 +919,7 @@ ${args.instruction}`
860
919
  }
861
920
 
862
921
  // src/agent-task/checks/agent-session.ts
863
- import { createHash as createHash2 } from "crypto";
922
+ import { createHash as createHash3 } from "crypto";
864
923
 
865
924
  // src/agent-task/tracing.ts
866
925
  function createNoopAgentTaskTraceContext() {
@@ -908,7 +967,7 @@ function isTraceableSpanId(spanId) {
908
967
  }
909
968
 
910
969
  // src/agent-task/checks/mcp-tools.ts
911
- import { createHash } from "crypto";
970
+ import { createHash as createHash2 } from "crypto";
912
971
  import { isAbsolute, resolve } from "path";
913
972
  import { readFile } from "fs/promises";
914
973
  var DEFAULT_TOOL_TIMEOUT_MS = 3e4;
@@ -1065,7 +1124,7 @@ function validateMcpServerConfig(server, where) {
1065
1124
  }
1066
1125
  }
1067
1126
  function sha256(text) {
1068
- return createHash("sha256").update(text).digest("hex");
1127
+ return createHash2("sha256").update(text).digest("hex");
1069
1128
  }
1070
1129
  function renderValue(value) {
1071
1130
  if (typeof value === "string") return value;
@@ -1259,8 +1318,17 @@ var RECORD_TEXT_LIMIT = 2 * 1024;
1259
1318
  var RECORD_BRIEFING_LIMIT = 16 * 1024;
1260
1319
  var RECORD_MAX_STEPS = 64;
1261
1320
  var RECORD_KEEP_HEAD = 56;
1321
+ function sessionPrefix(body) {
1322
+ const messages = body.messages ?? [];
1323
+ const firstUser = messages.find((m) => m.role === "user");
1324
+ return [
1325
+ String(body.model),
1326
+ ...messages.filter((m) => m.role === "system").map((m) => m.content),
1327
+ firstUser?.content
1328
+ ].map((part) => typeof part === "string" ? part : JSON.stringify(part ?? null)).join("\0");
1329
+ }
1262
1330
  function sha2562(text) {
1263
- return createHash2("sha256").update(text).digest("hex");
1331
+ return createHash3("sha256").update(text).digest("hex");
1264
1332
  }
1265
1333
  function renderValue2(value) {
1266
1334
  if (typeof value === "string") return value;
@@ -1543,11 +1611,16 @@ async function runAgentSession(spec) {
1543
1611
  const provider = createOpenAICompatible({
1544
1612
  name: "openrouter",
1545
1613
  baseURL: spec.baseURL ?? "https://openrouter.ai/api/v1",
1614
+ fetch: fetchWithPromptCacheKey,
1546
1615
  apiKey: spec.apiKey ?? process.env.OPENROUTER_API_KEY ?? process.env.OPENAI_API_KEY,
1547
1616
  // Mark the briefing prefix cacheable — repeated samples (rejudge) re-bill
1548
- // nothing for the shared turn-0 context.
1617
+ // nothing for the shared turn-0 context — and keep every step of one
1618
+ // session on the replica holding its growing prefix. The key is per
1619
+ // session (briefing + criterion): sessions run concurrently, unlike
1620
+ // t.judge calls, and one key per task would pile them onto one replica.
1549
1621
  transformRequestBody: (body) => ({
1550
1622
  ...body,
1623
+ prompt_cache_key: promptCacheKey(sessionPrefix(body)),
1551
1624
  messages: body.messages?.map(
1552
1625
  (m) => m.role === "system" && typeof m.content === "string" ? { ...m, content: [{ type: "text", text: m.content, cache_control: { type: "ephemeral" } }] } : m
1553
1626
  )
@@ -2290,7 +2363,7 @@ var Recorder = class {
2290
2363
  };
2291
2364
 
2292
2365
  // src/agent-task/checks/compact.ts
2293
- import { createHash as createHash3 } from "crypto";
2366
+ import { createHash as createHash4 } from "crypto";
2294
2367
  var RECEIVED_VALUE_LIMIT = 4 * 1024;
2295
2368
  var JUDGE_SEGMENT_LIMIT = 16 * 1024;
2296
2369
  var PREVIEW_CHARS = 256;
@@ -2398,7 +2471,7 @@ function marker(bytes) {
2398
2471
  kind: "truncated",
2399
2472
  preview,
2400
2473
  size_bytes: bytes.length,
2401
- sha256: createHash3("sha256").update(bytes).digest("hex")
2474
+ sha256: createHash4("sha256").update(bytes).digest("hex")
2402
2475
  };
2403
2476
  }
2404
2477
  function truncateSessionText(session) {
@@ -3043,7 +3116,7 @@ function isFileArtifact(value) {
3043
3116
  }
3044
3117
 
3045
3118
  // src/agent-task/deliverables/upload.ts
3046
- import { createHash as createHash4 } from "crypto";
3119
+ import { createHash as createHash5 } from "crypto";
3047
3120
  import { createReadStream } from "fs";
3048
3121
  import { Readable } from "stream";
3049
3122
  var DEFAULT_FETCH = typeof fetch === "function" ? fetch : void 0;
@@ -3098,7 +3171,7 @@ async function uploadOne(name, artifact, config) {
3098
3171
  }
3099
3172
  async function hashFile(path) {
3100
3173
  return new Promise((resolve5, reject) => {
3101
- const hash = createHash4("sha256");
3174
+ const hash = createHash5("sha256");
3102
3175
  let size = 0;
3103
3176
  const stream = createReadStream(path);
3104
3177
  stream.on("data", (chunk) => {
@@ -4338,7 +4411,7 @@ function usageOf2(raw) {
4338
4411
  }
4339
4412
 
4340
4413
  // src/agent-task/transcript-replay/otlp.ts
4341
- import { createHash as createHash5 } from "crypto";
4414
+ import { createHash as createHash6 } from "crypto";
4342
4415
  var SPAN_KIND_INTERNAL = 1;
4343
4416
  var STATUS_CODE_UNSET = 0;
4344
4417
  function transcriptSessionToOtlp(session, options = {}) {
@@ -4485,7 +4558,7 @@ function messageAttrValue(role, content) {
4485
4558
  return JSON.stringify([{ role, content }]);
4486
4559
  }
4487
4560
  function sha256Hex(input) {
4488
- return createHash5("sha256").update(input).digest("hex");
4561
+ return createHash6("sha256").update(input).digest("hex");
4489
4562
  }
4490
4563
  function deriveTraceId(source, sessionId) {
4491
4564
  return sha256Hex(`apo-transcript-replay|${source}|${sessionId}`).slice(0, 32);
@@ -6148,12 +6221,12 @@ function printOverallSummary(summaries, hasErrors) {
6148
6221
  }
6149
6222
 
6150
6223
  // src/agent-task/task-revision-manifest.ts
6151
- import { createHash as createHash6 } from "crypto";
6224
+ import { createHash as createHash7 } from "crypto";
6152
6225
  function normalizeManifestPath(path) {
6153
6226
  return path.split("\\").join("/").normalize("NFC");
6154
6227
  }
6155
6228
  function sha256Hex2(bytes) {
6156
- return createHash6("sha256").update(bytes).digest("hex");
6229
+ return createHash7("sha256").update(bytes).digest("hex");
6157
6230
  }
6158
6231
  function compareBytewiseUtf8(a, b) {
6159
6232
  const ba = Buffer.from(a, "utf8");