@xn-intenton-z2a/agentic-lib 7.1.63 → 7.1.65

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xn-intenton-z2a/agentic-lib",
3
- "version": "7.1.63",
3
+ "version": "7.1.65",
4
4
  "description": "Agentic-lib Agentic Coding Systems SDK powering automated GitHub workflows.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -209,9 +209,44 @@ export function supportsReasoningEffort(model) {
209
209
  return MODELS_SUPPORTING_REASONING_EFFORT.has(model);
210
210
  }
211
211
 
212
+ /**
213
+ * Detect whether an error is an HTTP 429 Too Many Requests (rate limit) response.
214
+ * The Copilot SDK may surface this as an Error with a status, statusCode, or message.
215
+ *
216
+ * @param {unknown} err - The error to test
217
+ * @returns {boolean}
218
+ */
219
+ export function isRateLimitError(err) {
220
+ if (!err || typeof err !== "object") return false;
221
+ const status = err.status ?? err.statusCode ?? err.code;
222
+ if (status === 429 || status === "429") return true;
223
+ const msg = (err.message || "").toLowerCase();
224
+ return msg.includes("429") || msg.includes("too many requests") || msg.includes("rate limit");
225
+ }
226
+
227
+ /**
228
+ * Extract the retry delay in milliseconds from a rate-limit error.
229
+ * Tries the Retry-After header (seconds) first, then falls back to exponential backoff.
230
+ *
231
+ * @param {unknown} err - The error (may have headers or retryAfter)
232
+ * @param {number} attempt - Zero-based attempt number (0 = first retry)
233
+ * @param {number} [baseDelayMs=60000] - Base delay for exponential backoff (ms)
234
+ * @returns {number} Milliseconds to wait before retrying
235
+ */
236
+ export function retryDelayMs(err, attempt, baseDelayMs = 60000) {
237
+ const retryAfterHeader =
238
+ err?.headers?.["retry-after"] ?? err?.retryAfter ?? err?.response?.headers?.["retry-after"];
239
+ if (retryAfterHeader != null) {
240
+ const parsed = Number(retryAfterHeader);
241
+ if (!isNaN(parsed) && parsed > 0) return parsed * 1000;
242
+ }
243
+ return baseDelayMs * Math.pow(2, attempt);
244
+ }
245
+
212
246
  /**
213
247
  * Run a Copilot SDK session and return the response.
214
248
  * Handles the full lifecycle: create client → create session → send → stop.
249
+ * Retries automatically on HTTP 429 rate-limit responses (up to maxRetries times).
215
250
  *
216
251
  * @param {Object} options
217
252
  * @param {string} options.model - Copilot SDK model name
@@ -221,6 +256,7 @@ export function supportsReasoningEffort(model) {
221
256
  * @param {string} [options.githubToken] - Optional token; falls back to COPILOT_GITHUB_TOKEN env var.
222
257
  * @param {Object} [options.tuning] - Tuning config (reasoningEffort, infiniteSessions)
223
258
  * @param {string} [options.profileName] - Profile name for logging
259
+ * @param {number} [options.maxRetries=3] - Maximum number of retry attempts on rate-limit errors
224
260
  * @returns {Promise<{content: string, tokensUsed: number}>}
225
261
  */
226
262
  export async function runCopilotTask({
@@ -231,6 +267,38 @@ export async function runCopilotTask({
231
267
  githubToken,
232
268
  tuning,
233
269
  profileName,
270
+ maxRetries = 3,
271
+ }) {
272
+ const profile = profileName || "unknown";
273
+
274
+ // Attempt 0 is the initial call; attempts 1..maxRetries are retries after 429s.
275
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
276
+ try {
277
+ return await _runCopilotTaskOnce({ model, systemMessage, prompt, writablePaths, githubToken, tuning, profileName: profile });
278
+ } catch (err) {
279
+ if (isRateLimitError(err) && attempt < maxRetries) {
280
+ const delayMs = retryDelayMs(err, attempt);
281
+ core.warning(
282
+ `[copilot] Rate limit (429) hit — waiting ${Math.round(delayMs / 1000)}s before retry ${attempt + 1}/${maxRetries}`,
283
+ );
284
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
285
+ } else {
286
+ throw err;
287
+ }
288
+ }
289
+ }
290
+ // Unreachable: loop always returns or throws, but satisfies static analysis.
291
+ throw new Error("[copilot] runCopilotTask: all retry attempts exhausted");
292
+ }
293
+
294
+ async function _runCopilotTaskOnce({
295
+ model,
296
+ systemMessage,
297
+ prompt,
298
+ writablePaths,
299
+ githubToken,
300
+ tuning,
301
+ profileName,
234
302
  }) {
235
303
  const profile = profileName || "unknown";
236
304
  core.info(
@@ -25,11 +25,10 @@ export async function maintainLibrary(context) {
25
25
  return { outcome: "nop", details: "Mission already complete (MISSION_COMPLETE.md signal)" };
26
26
  }
27
27
 
28
- const sources = readOptionalFile(config.paths.librarySources.path);
29
- if (!sources.trim()) {
30
- core.info("No sources found. Returning nop.");
31
- return { outcome: "nop", details: "No SOURCES.md or empty" };
32
- }
28
+ const sourcesPath = config.paths.librarySources.path;
29
+ const sources = readOptionalFile(sourcesPath);
30
+ const mission = readOptionalFile(config.paths.mission.path);
31
+ const hasUrls = /https?:\/\//.test(sources);
33
32
 
34
33
  const libraryPath = config.paths.library.path;
35
34
  const libraryLimit = config.paths.library.limit;
@@ -37,26 +36,50 @@ export async function maintainLibrary(context) {
37
36
 
38
37
  const agentInstructions = instructions || "Maintain the library by updating documents from sources.";
39
38
 
40
- const prompt = [
41
- "## Instructions",
42
- agentInstructions,
43
- "",
44
- "## Sources",
45
- sources,
46
- "",
47
- `## Current Library Documents (${libraryDocs.length}/${libraryLimit} max)`,
48
- ...libraryDocs.map((d) => `### ${d.name}\n${d.content}`),
49
- "",
50
- "## Your Task",
51
- "1. Read each URL in SOURCES.md and extract technical content.",
52
- "2. Create or update library documents based on the source content.",
53
- "3. Remove library documents that no longer have corresponding sources.",
54
- "",
55
- formatPathsSection(writablePaths, config.readOnlyPaths, config),
56
- "",
57
- "## Constraints",
58
- `- Maximum ${libraryLimit} library documents`,
59
- ].join("\n");
39
+ let prompt;
40
+ if (!hasUrls) {
41
+ // SOURCES.md has no URLs — ask the LLM to find relevant sources based on the mission
42
+ core.info("SOURCES.md has no URLs — asking LLM to discover relevant sources from the mission.");
43
+ prompt = [
44
+ "## Instructions",
45
+ agentInstructions,
46
+ "",
47
+ "## Mission",
48
+ mission || "(no mission file found)",
49
+ "",
50
+ "## Current SOURCES.md",
51
+ sources || "(empty)",
52
+ "",
53
+ "## Your Task",
54
+ `SOURCES.md has no URLs yet. Research the mission above and populate ${sourcesPath} with 3-8 relevant reference URLs.`,
55
+ "Find documentation, tutorials, API references, Wikipedia articles, or npm packages related to the mission's core topic.",
56
+ "Use web search to discover high-quality, stable URLs (prefer official docs, Wikipedia, MDN, npm).",
57
+ `Write the URLs as a markdown list in ${sourcesPath}, keeping the existing header text.`,
58
+ "",
59
+ formatPathsSection(writablePaths, config.readOnlyPaths, config),
60
+ ].join("\n");
61
+ } else {
62
+ prompt = [
63
+ "## Instructions",
64
+ agentInstructions,
65
+ "",
66
+ "## Sources",
67
+ sources,
68
+ "",
69
+ `## Current Library Documents (${libraryDocs.length}/${libraryLimit} max)`,
70
+ ...libraryDocs.map((d) => `### ${d.name}\n${d.content}`),
71
+ "",
72
+ "## Your Task",
73
+ "1. Read each URL in SOURCES.md and extract technical content.",
74
+ "2. Create or update library documents based on the source content.",
75
+ "3. Remove library documents that no longer have corresponding sources.",
76
+ "",
77
+ formatPathsSection(writablePaths, config.readOnlyPaths, config),
78
+ "",
79
+ "## Constraints",
80
+ `- Maximum ${libraryLimit} library documents`,
81
+ ].join("\n");
82
+ }
60
83
 
61
84
  const { tokensUsed, inputTokens, outputTokens, cost } = await runCopilotTask({
62
85
  model,
@@ -67,13 +90,18 @@ export async function maintainLibrary(context) {
67
90
  tuning: t,
68
91
  });
69
92
 
93
+ const outcomeLabel = hasUrls ? "library-maintained" : "sources-discovered";
94
+ const detailsMsg = hasUrls
95
+ ? `Maintained library (${libraryDocs.length} docs, limit ${libraryLimit})`
96
+ : `Discovered sources for SOURCES.md from mission`;
97
+
70
98
  return {
71
- outcome: "library-maintained",
99
+ outcome: outcomeLabel,
72
100
  tokensUsed,
73
101
  inputTokens,
74
102
  outputTokens,
75
103
  cost,
76
104
  model,
77
- details: `Maintained library (${libraryDocs.length} docs, limit ${libraryLimit})`,
105
+ details: detailsMsg,
78
106
  };
79
107
  }
@@ -4,5 +4,3 @@ Reference material and documentation sources for this project.
4
4
 
5
5
  Add URLs, papers, API docs, or other reference material here.
6
6
  The maintain-library workflow will process these into `library/` documents.
7
-
8
- - https://github.com/xn-intenton-z2a/repository0
@@ -16,7 +16,7 @@
16
16
  "author": "",
17
17
  "license": "MIT",
18
18
  "dependencies": {
19
- "@xn-intenton-z2a/agentic-lib": "^7.1.63"
19
+ "@xn-intenton-z2a/agentic-lib": "^7.1.65"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@vitest/coverage-v8": "^4.0.18",