@particle-academy/prism-harness 0.9.1 → 0.10.0
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 +18 -3
- package/dist/runtime.d.ts +3 -1
- package/dist/runtime.js +7 -1
- package/dist/stores/file-owner-lock.js +3 -1
- package/dist/stores/file-session-store.js +37 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -58,9 +58,17 @@ creates no session, thread or index entry and makes no model/provider request
|
|
|
58
58
|
or logical state change. Reads may physically purge expired entries, exactly as
|
|
59
59
|
an ordinary store read does. Explicit state writes during resolution refuse.
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
names
|
|
63
|
-
|
|
61
|
+
The mode list is the ceiling for named and legacy sessions. Caller-supplied
|
|
62
|
+
names narrow it; omitting names uses the mode's full list, and an empty list
|
|
63
|
+
offers no tools. A requested `*` selects the mode's ceiling. Other names outside
|
|
64
|
+
the ceiling fail before the model is called.
|
|
65
|
+
|
|
66
|
+
Legacy callers that intentionally need the previous replacement behavior can
|
|
67
|
+
set `AgentRuntimeOptions.allowLegacyToolListWidening` to `true`. This permits
|
|
68
|
+
widening only in the legacy namespace; the configured authorizer still filters
|
|
69
|
+
the offer and tool calls. Named agents always retain their ceiling. Remove an
|
|
70
|
+
old explicit `narrowRequestedTools: false`: it now raises a migration error.
|
|
71
|
+
Explicit `true` remains supported, and conflicts with the widening opt-out.
|
|
64
72
|
Named agents inherit no legacy authority. Builtins require explicit registration and
|
|
65
73
|
mode selection. Named-agent duplicates are refused; use
|
|
66
74
|
tools.forAgent('support').replace(replacement, 'app.support', 'app.support.v2')
|
|
@@ -393,6 +401,13 @@ badly.
|
|
|
393
401
|
native boot/PID realm and a proven dead or explicitly released owner. Local
|
|
394
402
|
filesystems on one host only; SMB/NFS are unsupported.
|
|
395
403
|
|
|
404
|
+
State readers close their handles before decoding the document. On Windows,
|
|
405
|
+
replacement of a state file can fail while another process holds it open,
|
|
406
|
+
even with delete sharing. Permission failures retry for up to 750 ms. If the
|
|
407
|
+
reader stays open or access is permanently denied, the original native error
|
|
408
|
+
is raised and the temporary file is removed; the previous state stays intact.
|
|
409
|
+
Other errors and POSIX replacement do not retry. Callbacks are never replayed.
|
|
410
|
+
|
|
396
411
|
On Windows, the first file-lock operation makes one fixed, read-only metadata
|
|
397
412
|
query using the system Windows PowerShell and CIM module, with a fifteen-second
|
|
398
413
|
timeout. Established identities and detected container conditions are cached
|
package/dist/runtime.d.ts
CHANGED
|
@@ -110,8 +110,10 @@ export interface PendingApproval {
|
|
|
110
110
|
arguments: JsonObject;
|
|
111
111
|
}
|
|
112
112
|
export interface AgentRuntimeOptions {
|
|
113
|
-
/**
|
|
113
|
+
/** Explicit true retains strict behavior. Explicit false now requires migration to the widening opt-out. */
|
|
114
114
|
narrowRequestedTools?: boolean;
|
|
115
|
+
/** Permit caller names to replace a LEGACY session's mode list. Named agents remain strict; authorizers still apply. */
|
|
116
|
+
allowLegacyToolListWidening?: boolean;
|
|
115
117
|
config?: RuntimeConfig;
|
|
116
118
|
agents?: Record<string, RuntimeConfig>;
|
|
117
119
|
client: LlmClient;
|
package/dist/runtime.js
CHANGED
|
@@ -39,7 +39,13 @@ export class AgentRuntime {
|
|
|
39
39
|
constructor(options) {
|
|
40
40
|
if (options.narrowRequestedTools !== undefined && typeof options.narrowRequestedTools !== 'boolean')
|
|
41
41
|
throw new TypeError('narrowRequestedTools must be a boolean');
|
|
42
|
-
|
|
42
|
+
if (options.allowLegacyToolListWidening !== undefined && typeof options.allowLegacyToolListWidening !== 'boolean')
|
|
43
|
+
throw new TypeError('allowLegacyToolListWidening must be a boolean');
|
|
44
|
+
if (options.narrowRequestedTools === false)
|
|
45
|
+
throw new TypeError('narrowRequestedTools: false is no longer supported. Remove it; use allowLegacyToolListWidening: true only to explicitly permit legacy tool-list widening.');
|
|
46
|
+
if (options.narrowRequestedTools === true && options.allowLegacyToolListWidening === true)
|
|
47
|
+
throw new TypeError('narrowRequestedTools: true conflicts with allowLegacyToolListWidening: true.');
|
|
48
|
+
this.#narrowRequestedTools = options.allowLegacyToolListWidening !== true;
|
|
43
49
|
this.#client = options.client;
|
|
44
50
|
this.#modes = options.modes;
|
|
45
51
|
this.#tools = options.tools;
|
|
@@ -126,6 +126,9 @@ async function inspect(lockPath, local) {
|
|
|
126
126
|
return 'held';
|
|
127
127
|
if (!owner.released && !lockPidIsDead(owner.pid))
|
|
128
128
|
return 'held';
|
|
129
|
+
// A proven stale owner's temporary file may still be open on Windows.
|
|
130
|
+
// Cleanup errors use inspection's held handling and the existing wait budget.
|
|
131
|
+
await cleanFileLockOwner(lockPath, owner, temporary);
|
|
129
132
|
}
|
|
130
133
|
catch (error) {
|
|
131
134
|
if (code(error) === 'ENOENT')
|
|
@@ -134,7 +137,6 @@ async function inspect(lockPath, local) {
|
|
|
134
137
|
return 'held';
|
|
135
138
|
throw error;
|
|
136
139
|
}
|
|
137
|
-
await cleanFileLockOwner(lockPath, owner, temporary);
|
|
138
140
|
return 'retry';
|
|
139
141
|
}
|
|
140
142
|
async function release(progress) {
|
|
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { mkdirSync } from 'node:fs';
|
|
3
3
|
import { mkdir, open, readFile, rename, rm, stat, truncate, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
6
|
import { isJsonObject } from '../json.js';
|
|
6
7
|
import { HarnessError } from '../errors.js';
|
|
7
8
|
import { withFileOwnerLock, pendingFileOwnerReleases } from './file-owner-lock.js';
|
|
@@ -52,6 +53,8 @@ export class FileSessionStore {
|
|
|
52
53
|
async get(key) {
|
|
53
54
|
let raw;
|
|
54
55
|
try {
|
|
56
|
+
// Node's whole-file helper closes its own handle on both outcomes before
|
|
57
|
+
// resolving; JSON decoding and expiry housekeeping happen after it closes.
|
|
55
58
|
raw = await readFile(this.#pathFor(key), 'utf8');
|
|
56
59
|
}
|
|
57
60
|
catch (error) {
|
|
@@ -91,7 +94,7 @@ export class FileSessionStore {
|
|
|
91
94
|
try {
|
|
92
95
|
// Atomic on both POSIX and Windows. A reader either sees the whole old
|
|
93
96
|
// payload or the whole new one, never half of either.
|
|
94
|
-
await
|
|
97
|
+
await replaceStateFile(temporary, target);
|
|
95
98
|
}
|
|
96
99
|
catch (error) {
|
|
97
100
|
await rm(temporary, { force: true });
|
|
@@ -255,3 +258,36 @@ function digestOf(key) {
|
|
|
255
258
|
function isMissing(error) {
|
|
256
259
|
return typeof error === 'object' && error !== null && error.code === 'ENOENT';
|
|
257
260
|
}
|
|
261
|
+
/** An open Windows target can block replacement even with delete sharing. */
|
|
262
|
+
async function replaceStateFile(temporary, target) {
|
|
263
|
+
try {
|
|
264
|
+
await rename(temporary, target);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
catch (original) {
|
|
268
|
+
if (process.platform !== 'win32' || !isReplacePermissionError(original))
|
|
269
|
+
throw original;
|
|
270
|
+
const deadline = performance.now() + 750;
|
|
271
|
+
while (true) {
|
|
272
|
+
const remaining = deadline - performance.now();
|
|
273
|
+
if (remaining <= 0)
|
|
274
|
+
throw original;
|
|
275
|
+
// This timer stays referenced: an awaited write must keep the process alive.
|
|
276
|
+
await delay(Math.min(10, remaining));
|
|
277
|
+
if (performance.now() >= deadline)
|
|
278
|
+
throw original;
|
|
279
|
+
try {
|
|
280
|
+
await rename(temporary, target);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
if (!isReplacePermissionError(error))
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function isReplacePermissionError(error) {
|
|
291
|
+
const code = typeof error === 'object' && error !== null ? error.code : undefined;
|
|
292
|
+
return code === 'EPERM' || code === 'EACCES';
|
|
293
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@particle-academy/prism-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Durable agent sessions for TypeScript — threads, session state and store drivers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/node": "^26.3.0",
|
|
27
27
|
"typescript": "^5.9.3",
|
|
28
|
-
"
|
|
28
|
+
"vite": "^8.3.5",
|
|
29
|
+
"vitest": "^4.1.12"
|
|
29
30
|
},
|
|
30
31
|
"repository": {
|
|
31
32
|
"type": "git",
|