@mytegroupinc/myte-core 0.0.16 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -139,3 +139,5 @@ Examples:
139
139
  - `npx myte update-client --subject "Weekly client update" --body-file ./updates/week-12.md`
140
140
 
141
141
  This package is published under the org scope for governance; the public `myte` wrapper delegates here.
142
+
143
+ `--json-response` sends both the Myte convenience flag (`myte_json_response: true`) and the OpenAI-compatible chat-completions shape (`response_format: { type: "json_object" }`). On the public developer API, that strict-JSON path also defaults to `medium` reasoning unless the caller explicitly overrides reasoning.
package/cli.js CHANGED
@@ -275,7 +275,7 @@ function printHelp() {
275
275
  " --timeout-ms <ms> Request timeout (default: 300000)",
276
276
  " --base-url <url> API base (default: https://api.myte.dev)",
277
277
  " --payload-file <path> Raw OpenAI-style chat-completions payload for `myte ai`",
278
- " --json-response Ask the Myte AI gateway to return clean JSON only",
278
+ " --json-response Ask the Myte AI gateway to return clean JSON only and send OpenAI-compatible response_format",
279
279
  " --max-output-tokens Output token cap for `myte ai` simple queries",
280
280
  " --temperature <num> Temperature for `myte ai` simple queries",
281
281
  " --output-dir <path> Command Center output directory (default: <wrapper-root>/MyteCommandCenter)",
@@ -1067,9 +1067,15 @@ async function fetchQaqcSyncSnapshot({ apiBase, key, timeoutMs }) {
1067
1067
  );
1068
1068
 
1069
1069
  if (!resp.ok || body.status !== "success") {
1070
+ const retryAfter = resp.headers?.get?.("retry-after");
1070
1071
  const msg = body?.message || `QAQC sync request failed (${resp.status})`;
1071
- const err = new Error(msg);
1072
+ const err = new Error(
1073
+ retryAfter
1074
+ ? `${msg} Retry after ${retryAfter}s.`
1075
+ : msg
1076
+ );
1072
1077
  err.status = resp.status;
1078
+ if (retryAfter) err.retryAfter = retryAfter;
1073
1079
  throw err;
1074
1080
  }
1075
1081
  return body.data || {};
@@ -1227,15 +1233,30 @@ async function fetchRunQaqcBatchStatus({ apiBase, key, timeoutMs, batchId }) {
1227
1233
  );
1228
1234
 
1229
1235
  if (!resp.ok || body.status !== "success") {
1236
+ const retryAfter = resp.headers?.get?.("retry-after");
1230
1237
  const msg = body?.message || `Run QAQC status request failed (${resp.status})`;
1231
- const err = new Error(msg);
1238
+ const err = new Error(
1239
+ retryAfter
1240
+ ? `${msg} Retry after ${retryAfter}s.`
1241
+ : msg
1242
+ );
1232
1243
  err.status = resp.status;
1244
+ if (retryAfter) err.retryAfter = retryAfter;
1233
1245
  throw err;
1234
1246
  }
1235
1247
  return body.data || {};
1236
1248
  }
1237
1249
 
1238
- async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint = "/project-assistant/query" }) {
1250
+ function resolveRetryAfterMs(err, fallbackMs = 5_000) {
1251
+ const retryAfterRaw = firstNonEmptyString(err?.retryAfter);
1252
+ const retryAfterSeconds = Number.parseInt(String(retryAfterRaw || ""), 10);
1253
+ if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0) {
1254
+ return Math.min(retryAfterSeconds * 1_000, 60_000);
1255
+ }
1256
+ return fallbackMs;
1257
+ }
1258
+
1259
+ async function createAssistantQueryJob({ apiBase, key, payload, timeoutMs, endpoint = "/project-assistant/query" }) {
1239
1260
  const fetchFn = await getFetch();
1240
1261
  const url = `${apiBase}${endpoint}`;
1241
1262
  const { resp, body } = await fetchJsonWithTimeout(
@@ -1253,9 +1274,35 @@ async function callAssistantQuery({ apiBase, key, payload, timeoutMs, endpoint =
1253
1274
  );
1254
1275
 
1255
1276
  if (!resp.ok || body.status !== "success") {
1277
+ const retryAfter = resp.headers?.get?.("retry-after");
1256
1278
  const msg = body?.message || `Query failed (${resp.status})`;
1257
- const err = new Error(msg);
1279
+ const err = new Error(retryAfter ? `${msg} Retry after ${retryAfter}s.` : msg);
1280
+ err.status = resp.status;
1281
+ if (retryAfter) err.retryAfter = retryAfter;
1282
+ throw err;
1283
+ }
1284
+ return body.data || {};
1285
+ }
1286
+
1287
+ async function fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId }) {
1288
+ const fetchFn = await getFetch();
1289
+ const url = `${apiBase}/project-assistant/query/${encodeURIComponent(String(jobId || ""))}`;
1290
+ const { resp, body } = await fetchJsonWithTimeout(
1291
+ fetchFn,
1292
+ url,
1293
+ {
1294
+ method: "GET",
1295
+ headers: { Authorization: `Bearer ${key}` },
1296
+ },
1297
+ timeoutMs
1298
+ );
1299
+
1300
+ if (!resp.ok || body.status !== "success") {
1301
+ const retryAfter = resp.headers?.get?.("retry-after");
1302
+ const msg = body?.message || `Query status request failed (${resp.status})`;
1303
+ const err = new Error(retryAfter ? `${msg} Retry after ${retryAfter}s.` : msg);
1258
1304
  err.status = resp.status;
1305
+ if (retryAfter) err.retryAfter = retryAfter;
1259
1306
  throw err;
1260
1307
  }
1261
1308
  return body.data || {};
@@ -1326,6 +1373,10 @@ async function runRunQaqc(args) {
1326
1373
  try {
1327
1374
  finalStatus = await fetchRunQaqcBatchStatus({ apiBase, key, timeoutMs, batchId: data.batch_id });
1328
1375
  } catch (err) {
1376
+ if (Number(err?.status) === 429) {
1377
+ await sleep(resolveRetryAfterMs(err));
1378
+ continue;
1379
+ }
1329
1380
  console.error("Failed to fetch QAQC batch status:", err?.message || err);
1330
1381
  process.exit(1);
1331
1382
  }
@@ -1341,11 +1392,18 @@ async function runRunQaqc(args) {
1341
1392
 
1342
1393
  if (shouldSync && ["completed", "completed_with_errors"].includes(String(finalStatus.status || "").trim())) {
1343
1394
  let snapshot;
1344
- try {
1345
- snapshot = await fetchQaqcSyncSnapshot({ apiBase, key, timeoutMs });
1346
- } catch (err) {
1347
- console.error("Failed to fetch QAQC sync snapshot after batch completion:", err?.message || err);
1348
- process.exit(1);
1395
+ for (let attempt = 0; attempt < 5; attempt += 1) {
1396
+ try {
1397
+ snapshot = await fetchQaqcSyncSnapshot({ apiBase, key, timeoutMs });
1398
+ break;
1399
+ } catch (err) {
1400
+ if (Number(err?.status) === 429 && attempt < 4) {
1401
+ await sleep(resolveRetryAfterMs(err));
1402
+ continue;
1403
+ }
1404
+ console.error("Failed to fetch QAQC sync snapshot after batch completion:", err?.message || err);
1405
+ process.exit(1);
1406
+ }
1349
1407
  }
1350
1408
 
1351
1409
  let resolved;
@@ -1668,6 +1726,20 @@ function stableItemId(item, keys, fallback) {
1668
1726
  return fallback;
1669
1727
  }
1670
1728
 
1729
+ function bootstrapPathId(item, preferredKeys, fallbackKeys, fallback) {
1730
+ return stableItemId(item, preferredKeys, stableItemId(item, fallbackKeys, fallback));
1731
+ }
1732
+
1733
+ function bootstrapScopedPathId(item, preferredKeys, scopeKeys, fallbackKeys, fallback) {
1734
+ const preferred = stableItemId(item, preferredKeys, "");
1735
+ if (preferred) return preferred;
1736
+ const fallbackId = stableItemId(item, fallbackKeys, fallback);
1737
+ const scopeParts = (Array.isArray(scopeKeys) ? scopeKeys : [])
1738
+ .map((key) => String(item?.[key] || "").trim())
1739
+ .filter(Boolean);
1740
+ return [...scopeParts, fallbackId].filter(Boolean).join("__") || fallbackId;
1741
+ }
1742
+
1671
1743
  function stringifyYaml(value) {
1672
1744
  // eslint-disable-next-line global-require
1673
1745
  const YAML = require("yaml");
@@ -1889,20 +1961,42 @@ function writeBootstrapSnapshot({ snapshot, wrapperRoot, outputDir }) {
1889
1961
  const missions = Array.isArray(snapshot.missions) ? snapshot.missions.map((item) => scrubBootstrapValue(item)) : [];
1890
1962
 
1891
1963
  phases.forEach((phase, index) => {
1892
- const phaseId = stableItemId(phase, ["phase_id", "id"], `P${String(index + 1).padStart(3, "0")}`);
1893
- writeYamlFile(path.join(phasesDir, `${phaseId}.yml`), phase);
1964
+ const phasePathId = bootstrapPathId(
1965
+ phase,
1966
+ ["phase_key", "id"],
1967
+ ["phase_id"],
1968
+ `phase-${String(index + 1).padStart(3, "0")}`
1969
+ );
1970
+ writeYamlFile(path.join(phasesDir, `${phasePathId}.yml`), phase);
1894
1971
  });
1895
1972
  epics.forEach((epic, index) => {
1896
- const epicId = stableItemId(epic, ["epic_id", "id"], `E${String(index + 1).padStart(3, "0")}`);
1897
- writeYamlFile(path.join(epicsDir, `${epicId}.yml`), epic);
1973
+ const epicPathId = bootstrapScopedPathId(
1974
+ epic,
1975
+ ["epic_key", "id"],
1976
+ ["phase_key", "phase_id"],
1977
+ ["epic_id"],
1978
+ `epic-${String(index + 1).padStart(3, "0")}`
1979
+ );
1980
+ writeYamlFile(path.join(epicsDir, `${epicPathId}.yml`), epic);
1898
1981
  });
1899
1982
  stories.forEach((story, index) => {
1900
- const storyId = stableItemId(story, ["story_id", "id"], `S${String(index + 1).padStart(3, "0")}`);
1901
- writeYamlFile(path.join(storiesDir, `${storyId}.yml`), story);
1983
+ const storyPathId = bootstrapScopedPathId(
1984
+ story,
1985
+ ["story_key", "id"],
1986
+ ["phase_key", "phase_id", "epic_key", "epic_id"],
1987
+ ["story_id"],
1988
+ `story-${String(index + 1).padStart(3, "0")}`
1989
+ );
1990
+ writeYamlFile(path.join(storiesDir, `${storyPathId}.yml`), story);
1902
1991
  });
1903
1992
  missions.forEach((mission, index) => {
1904
- const missionId = stableItemId(mission, ["mission_id", "id"], `M${String(index + 1).padStart(3, "0")}`);
1905
- writeYamlFile(path.join(missionsDir, `${missionId}.yml`), mission);
1993
+ const missionPathId = bootstrapPathId(
1994
+ mission,
1995
+ ["mission_key", "id"],
1996
+ ["mission_id"],
1997
+ `mission-${String(index + 1).padStart(3, "0")}`
1998
+ );
1999
+ writeYamlFile(path.join(missionsDir, `${missionPathId}.yml`), mission);
1906
2000
  });
1907
2001
 
1908
2002
  if (snapshot.project && typeof snapshot.project === "object") {
@@ -3815,7 +3909,46 @@ async function runQuery(args) {
3815
3909
 
3816
3910
  let data;
3817
3911
  try {
3818
- data = await callAssistantQuery({ apiBase, key, payload, timeoutMs });
3912
+ const queued = await createAssistantQueryJob({ apiBase, key, payload, timeoutMs });
3913
+ if (queued.answer) {
3914
+ data = queued;
3915
+ } else {
3916
+ const jobId = firstNonEmptyString(queued.job_id, queued.id);
3917
+ if (!jobId) {
3918
+ throw new Error("Query job was accepted without a job_id.");
3919
+ }
3920
+ const pollTimeoutMs = Math.max(timeoutMs, 900_000);
3921
+ const startedAt = Date.now();
3922
+ let finalStatus = null;
3923
+ do {
3924
+ await sleep(2_000);
3925
+ try {
3926
+ finalStatus = await fetchAssistantQueryJobStatus({ apiBase, key, timeoutMs, jobId });
3927
+ } catch (err) {
3928
+ if (Number(err?.status) === 429) {
3929
+ await sleep(resolveRetryAfterMs(err));
3930
+ continue;
3931
+ }
3932
+ throw err;
3933
+ }
3934
+ if (["completed", "failed"].includes(String(finalStatus.status || "").trim())) {
3935
+ break;
3936
+ }
3937
+ } while (Date.now() - startedAt < pollTimeoutMs);
3938
+
3939
+ if (!finalStatus || !["completed", "failed"].includes(String(finalStatus.status || "").trim())) {
3940
+ throw new Error(`Timed out waiting for query job ${jobId}`);
3941
+ }
3942
+ if (String(finalStatus.status || "").trim() === "failed") {
3943
+ const detail = firstNonEmptyString(finalStatus?.error?.message, finalStatus?.error?.code);
3944
+ throw new Error(detail || `Query job ${jobId} failed`);
3945
+ }
3946
+ data = {
3947
+ answer: finalStatus.answer,
3948
+ context_blocks: finalStatus.context_blocks,
3949
+ telemetry: finalStatus.telemetry,
3950
+ };
3951
+ }
3819
3952
  } catch (err) {
3820
3953
  if (err?.name === "AbortError") {
3821
3954
  console.error(`Request timed out after ${timeoutMs}ms`);
package/lib/ai-gateway.js CHANGED
@@ -16,7 +16,10 @@ function buildSimpleAiPayload({ query, jsonResponse = false, maxOutputTokens, te
16
16
  const payload = {
17
17
  messages: [{ role: "user", content: String(query || "").trim() }],
18
18
  };
19
- if (jsonResponse) payload.myte_json_response = true;
19
+ if (jsonResponse) {
20
+ payload.myte_json_response = true;
21
+ payload.response_format = { type: "json_object" };
22
+ }
20
23
  if (Number.isFinite(Number(maxOutputTokens)) && Number(maxOutputTokens) > 0) {
21
24
  payload.max_tokens = Number(maxOutputTokens);
22
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mytegroupinc/myte-core",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "Myte CLI core implementation (Project Assistant + Myte AI gateway).",
5
5
  "type": "commonjs",
6
6
  "main": "cli.js",