agent-accelerator 0.5.7 → 0.5.8
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 +4 -4
- package/package.json +1 -1
- package/src/agent/agent.ts +76 -39
- package/src/agent/context.ts +3 -3
- package/src/agent/delegation.ts +12 -12
- package/src/agent/loop.ts +41 -41
- package/src/agent/subagent.ts +4 -4
- package/src/architecture.md +1 -1
- package/src/index.ts +7 -7
- package/src/models/catalog-cache.ts +7 -7
- package/src/models/catalog.ts +13 -13
- package/src/providers/google.ts +4 -4
- package/src/providers/openai-compat.ts +7 -7
- package/src/providers/openai.ts +5 -5
- package/src/providers/openrouter.ts +2 -2
- package/src/providers/shared.ts +4 -4
- package/src/scheduler/architecture.md +5 -2
- package/src/scheduler/scheduler.ts +111 -57
- package/src/scheduler/store.ts +273 -59
- package/src/session/architecture.md +1 -1
- package/src/session/store.ts +22 -22
- package/src/streaming/event-stream.ts +11 -11
- package/src/telemetry-file.ts +74 -24
- package/src/telemetry.ts +2 -2
- package/src/tools/executor.ts +1 -1
- package/src/tools/tool.ts +8 -8
- package/src/types/agent.ts +22 -20
- package/src/types/response.ts +8 -8
- package/src/types/scheduler.ts +2 -2
- package/src/types/tool.ts +5 -5
- package/src/update-models.ts +2 -2
- package/src/utils/architecture.md +1 -1
- package/src/utils/cache.ts +4 -4
- package/src/utils/durable.ts +78 -3
- package/src/utils/env.ts +1 -1
- package/src/utils/headers.ts +4 -4
- package/src/utils/home.ts +1 -1
- package/src/utils/retry.ts +1 -1
- package/src/utils/session.ts +5 -5
package/src/utils/durable.ts
CHANGED
|
@@ -91,7 +91,7 @@ export function readWithBackup(file: string, isValid: (text: string) => boolean)
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
// ---------------------------------------------------------------------------
|
|
94
|
-
// One process per folder
|
|
94
|
+
// One process per folder (used for session folders)
|
|
95
95
|
// ---------------------------------------------------------------------------
|
|
96
96
|
|
|
97
97
|
/** Folders this process already holds (the lock is re-entrant: many Agents share one home). */
|
|
@@ -103,8 +103,8 @@ function pause(ms: number): void {
|
|
|
103
103
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
function processAlive(pid: number): boolean {
|
|
106
|
+
/** True while process `pid` exists (EPERM means it exists but belongs to someone else); in: a pid from a lock or heartbeat file, out: the stale checks. */
|
|
107
|
+
export function processAlive(pid: number): boolean {
|
|
108
108
|
try {
|
|
109
109
|
process.kill(pid, 0);
|
|
110
110
|
return true;
|
|
@@ -216,3 +216,78 @@ export function lockFolder(folder: string): void {
|
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
|
+
|
|
220
|
+
/** Longest a short mutex may be held before it counts as stale even though its owner process is alive (a hung holder). */
|
|
221
|
+
const MUTEX_MAX_AGE_MS = 30_000;
|
|
222
|
+
/** Longest a caller waits for a short mutex before getting a ConfigError. */
|
|
223
|
+
const MUTEX_WAIT_MS = 10_000;
|
|
224
|
+
/** Mutex files this process holds right now; a second `withFileMutex` on the same file inside `fn` would deadlock, so it throws. */
|
|
225
|
+
const heldMutexes = new Set<string>();
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Runs `fn` while holding a short mutex file (`lockFile`, created with O_EXCL, content = pid, host, time), so several
|
|
229
|
+
* processes can change one shared file without losing updates. Hold it for milliseconds only: `fn` must be synchronous and
|
|
230
|
+
* quick. A waiter retries every few ms; a holder whose pid is gone (same host) or whose lock is older than 30 s is taken
|
|
231
|
+
* over. Not re-entrant (a nested call on the same file throws). The folder of `lockFile` must exist: if it is removed meanwhile
|
|
232
|
+
* (its owner deleted it) the call throws the plain `ENOENT` error, and the caller decides what that means. Throws
|
|
233
|
+
* `ConfigError` after 10 s of waiting. Local disks only.
|
|
234
|
+
*/
|
|
235
|
+
export function withFileMutex<T>(lockFile: string, fn: () => T): T {
|
|
236
|
+
const file = path.resolve(lockFile);
|
|
237
|
+
if (heldMutexes.has(file)) throw new ConfigError(`[Agent Accelerator] Mutex "${file}" is already held by this process (nested use).`);
|
|
238
|
+
const mine = JSON.stringify({ pid: process.pid, host: os.hostname(), at: Date.now() });
|
|
239
|
+
const deadline = Date.now() + MUTEX_WAIT_MS;
|
|
240
|
+
let unreadable = 0;
|
|
241
|
+
for (;;) {
|
|
242
|
+
try {
|
|
243
|
+
const fd = fs.openSync(file, "wx", 0o600);
|
|
244
|
+
try {
|
|
245
|
+
fs.writeFileSync(fd, mine);
|
|
246
|
+
} finally {
|
|
247
|
+
fs.closeSync(fd);
|
|
248
|
+
}
|
|
249
|
+
break;
|
|
250
|
+
} catch (err) {
|
|
251
|
+
if ((err as { code?: string }).code !== "EEXIST") throw err;
|
|
252
|
+
}
|
|
253
|
+
let text = "";
|
|
254
|
+
let mtime = 0;
|
|
255
|
+
try {
|
|
256
|
+
text = fs.readFileSync(file, "utf8");
|
|
257
|
+
mtime = fs.statSync(file).mtimeMs;
|
|
258
|
+
} catch {
|
|
259
|
+
continue; // released between the open and the read: try again
|
|
260
|
+
}
|
|
261
|
+
let owner: { pid?: unknown; host?: unknown } | undefined;
|
|
262
|
+
try {
|
|
263
|
+
owner = JSON.parse(text);
|
|
264
|
+
} catch {
|
|
265
|
+
owner = undefined;
|
|
266
|
+
}
|
|
267
|
+
let stale = false;
|
|
268
|
+
if (typeof owner?.pid !== "number") stale = ++unreadable > 5; // empty or garbage: another process may be mid-write
|
|
269
|
+
else if (owner.host === os.hostname() && !processAlive(owner.pid)) stale = true;
|
|
270
|
+
else if (Date.now() - mtime > MUTEX_MAX_AGE_MS) stale = true;
|
|
271
|
+
if (stale) {
|
|
272
|
+
try {
|
|
273
|
+
if (fs.readFileSync(file, "utf8") === text) fs.rmSync(file, { force: true });
|
|
274
|
+
} catch {
|
|
275
|
+
// why: it vanished: the next loop turn creates it.
|
|
276
|
+
}
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (Date.now() > deadline) throw new ConfigError(`[Agent Accelerator] Timed out after ${MUTEX_WAIT_MS} ms waiting for the lock "${file}" (held by process ${String(owner?.pid)}).`);
|
|
280
|
+
pause(typeof owner?.pid === "number" ? 2 : 40);
|
|
281
|
+
}
|
|
282
|
+
heldMutexes.add(file);
|
|
283
|
+
try {
|
|
284
|
+
return fn();
|
|
285
|
+
} finally {
|
|
286
|
+
heldMutexes.delete(file);
|
|
287
|
+
try {
|
|
288
|
+
fs.rmSync(file, { force: true });
|
|
289
|
+
} catch {
|
|
290
|
+
// why: a leftover mutex is detected as stale by the next caller.
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
package/src/utils/env.ts
CHANGED
package/src/utils/headers.ts
CHANGED
|
@@ -24,7 +24,7 @@ export function getPackageVersion(): string {
|
|
|
24
24
|
const v = (globalThis as any).process?.env?.npm_package_version;
|
|
25
25
|
if (typeof v === "string" && v) return v;
|
|
26
26
|
} catch {}
|
|
27
|
-
return "0.5.
|
|
27
|
+
return "0.5.8";
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// User-Agent string with version and platform; in: AGENT_ACCEL_USER_AGENT, out: a string.
|
|
@@ -43,7 +43,7 @@ function getAgentAccelUserAgent(): string {
|
|
|
43
43
|
export const AGENT_ACCEL_USER_AGENT = getAgentAccelUserAgent;
|
|
44
44
|
|
|
45
45
|
// ---------------------------------------------------------------------------
|
|
46
|
-
// Attribution
|
|
46
|
+
// Attribution: configurable, default none except OpenRouter legacy.
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
48
|
|
|
49
49
|
/** Configurable attribution headers (OpenRouter `HTTP-Referer` / `X-Title`). */
|
|
@@ -104,7 +104,7 @@ export function setHeaderCaseInsensitive(
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// ---------------------------------------------------------------------------
|
|
107
|
-
// Thought-signature namespacing
|
|
107
|
+
// Thought-signature namespacing.
|
|
108
108
|
// ---------------------------------------------------------------------------
|
|
109
109
|
|
|
110
110
|
/**
|
|
@@ -140,7 +140,7 @@ function stripForBrowser(headers: Record<string, string>): Record<string, string
|
|
|
140
140
|
/**
|
|
141
141
|
* Builds provider-specific session, cache-affinity, and attribution headers.
|
|
142
142
|
*
|
|
143
|
-
*
|
|
143
|
+
* Attribution is configurable via `setAttribution()` (default none; no
|
|
144
144
|
* referer/title is ever sent unless you set one). Custom (non-first-class)
|
|
145
145
|
* prefixes receive minimal `x-session-id`/`x-client-request-id` affinity only
|
|
146
146
|
* (no `session_id` alias, no attribution) when a session id is present.
|
package/src/utils/home.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { getEnv } from "./env.ts";
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The Agent Accelerator home: ONE folder that holds everything the SDK stores
|
|
6
|
-
* (`sessions/`, `
|
|
6
|
+
* (`sessions/`, `scheduled/`, `telemetry/`, `catalog/`), so nothing is
|
|
7
7
|
* scattered across the project or the package.
|
|
8
8
|
*/
|
|
9
9
|
let defaultHome: string | undefined;
|
package/src/utils/retry.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { isAbortError };
|
|
|
6
6
|
/**
|
|
7
7
|
* Checks whether a provider error is transient and safe to retry.
|
|
8
8
|
* Structured-data only: status / code / transient flag. Never message text,
|
|
9
|
-
* so a 400 mentioning "512 tokens" is not retried
|
|
9
|
+
* so a 400 mentioning "512 tokens" is not retried.
|
|
10
10
|
*/
|
|
11
11
|
export function isTransientError(error: unknown): boolean {
|
|
12
12
|
if (!error) return false;
|
package/src/utils/session.ts
CHANGED
|
@@ -66,7 +66,7 @@ export function createTrackingId(): string {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
* Monotonic clock for measuring durations
|
|
69
|
+
* Monotonic clock for measuring durations. Uses `performance.now()`
|
|
70
70
|
* when available, falling back to `Date.now()`. Differences on the same
|
|
71
71
|
* clock are valid durations; never treat the value as wall-clock epoch.
|
|
72
72
|
*
|
|
@@ -95,7 +95,7 @@ function random32Hex(): string {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
|
-
* New stable run id: 32 lowercase hex chars (crypto.randomUUID, no dashes)
|
|
98
|
+
* New stable run id: 32 lowercase hex chars (crypto.randomUUID, no dashes).
|
|
99
99
|
* Opaque lineage token attached via `(metadata as any).runId`.
|
|
100
100
|
*/
|
|
101
101
|
export function newRunId(): string {
|
|
@@ -103,7 +103,7 @@ export function newRunId(): string {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
|
-
* New stable turn id: 32 lowercase hex chars (crypto.randomUUID, no dashes)
|
|
106
|
+
* New stable turn id: 32 lowercase hex chars (crypto.randomUUID, no dashes).
|
|
107
107
|
* Opaque lineage token attached via `(metadata as any).turnId`.
|
|
108
108
|
*/
|
|
109
109
|
export function newTurnId(): string {
|
|
@@ -124,7 +124,7 @@ export function previewPrompt(prompt: string | ContentPart[]): string {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
/** Parsed model reference returned by {@link parseModelRef}
|
|
127
|
+
/** Parsed model reference returned by {@link parseModelRef}. */
|
|
128
128
|
export interface ParsedModelRef {
|
|
129
129
|
provider: string;
|
|
130
130
|
modelId: string;
|
|
@@ -161,7 +161,7 @@ function isModelSpec(v: unknown): v is ModelSpec {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
/**
|
|
164
|
-
* Single model-ref parser
|
|
164
|
+
* Single model-ref parser. Table-driven, no env sniffing:
|
|
165
165
|
* - string `"provider/model"` → split on first `/`; bare ids default to `openrouter`.
|
|
166
166
|
* - `ModelProviderInstance` (`{ model, apiKey?, baseUrl?, thinkingLevel? }`) → parse `model`, passthrough creds.
|
|
167
167
|
* - `ModelSpec` (`{ provider, id }`) → direct mapping.
|