agentfootprint 9.46.3 → 9.48.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/CLAUDE.md +49 -2
- package/dist/adapters/code/agentcore.js +35 -7
- package/dist/adapters/code/agentcore.js.map +1 -1
- package/dist/adapters/code/local.js +94 -10
- package/dist/adapters/code/local.js.map +1 -1
- package/dist/core/Agent.js +18 -1
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/agent/AgentBuilder.js +147 -3
- package/dist/core/agent/AgentBuilder.js.map +1 -1
- package/dist/core/agent/runManifest.js +7 -0
- package/dist/core/agent/runManifest.js.map +1 -1
- package/dist/core/checkin.js +16 -2
- package/dist/core/checkin.js.map +1 -1
- package/dist/core/pause.js +64 -1
- package/dist/core/pause.js.map +1 -1
- package/dist/doors/recipes.js +55 -0
- package/dist/doors/recipes.js.map +1 -0
- package/dist/esm/adapters/code/agentcore.js +35 -7
- package/dist/esm/adapters/code/agentcore.js.map +1 -1
- package/dist/esm/adapters/code/local.js +94 -10
- package/dist/esm/adapters/code/local.js.map +1 -1
- package/dist/esm/core/Agent.d.ts +9 -1
- package/dist/esm/core/Agent.js +19 -2
- package/dist/esm/core/Agent.js.map +1 -1
- package/dist/esm/core/agent/AgentBuilder.d.ts +68 -0
- package/dist/esm/core/agent/AgentBuilder.js +147 -3
- package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
- package/dist/esm/core/agent/runManifest.d.ts +18 -0
- package/dist/esm/core/agent/runManifest.js +7 -0
- package/dist/esm/core/agent/runManifest.js.map +1 -1
- package/dist/esm/core/checkin.d.ts +58 -0
- package/dist/esm/core/checkin.js +16 -2
- package/dist/esm/core/checkin.js.map +1 -1
- package/dist/esm/core/pause.d.ts +44 -0
- package/dist/esm/core/pause.js +61 -0
- package/dist/esm/core/pause.js.map +1 -1
- package/dist/esm/doors/recipes.d.ts +38 -0
- package/dist/esm/doors/recipes.js +39 -0
- package/dist/esm/doors/recipes.js.map +1 -0
- package/dist/esm/events/payloads.d.ts +29 -0
- package/dist/esm/hosting/conformance/cases.js +23 -4
- package/dist/esm/hosting/conformance/cases.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/observe.d.ts +2 -0
- package/dist/esm/observe.js +12 -0
- package/dist/esm/observe.js.map +1 -1
- package/dist/esm/recipes/apply.d.ts +67 -0
- package/dist/esm/recipes/apply.js +117 -0
- package/dist/esm/recipes/apply.js.map +1 -0
- package/dist/esm/recipes/defineAgentRecipe.d.ts +68 -0
- package/dist/esm/recipes/defineAgentRecipe.js +112 -0
- package/dist/esm/recipes/defineAgentRecipe.js.map +1 -0
- package/dist/esm/recipes/identifier.d.ts +40 -0
- package/dist/esm/recipes/identifier.js +95 -0
- package/dist/esm/recipes/identifier.js.map +1 -0
- package/dist/esm/recipes/index.d.ts +19 -0
- package/dist/esm/recipes/index.js +19 -0
- package/dist/esm/recipes/index.js.map +1 -0
- package/dist/esm/recipes/provenance.d.ts +57 -0
- package/dist/esm/recipes/provenance.js +53 -0
- package/dist/esm/recipes/provenance.js.map +1 -0
- package/dist/esm/recipes/types.d.ts +134 -0
- package/dist/esm/recipes/types.js +63 -0
- package/dist/esm/recipes/types.js.map +1 -0
- package/dist/esm/recipes/version.d.ts +38 -0
- package/dist/esm/recipes/version.js +84 -0
- package/dist/esm/recipes/version.js.map +1 -0
- package/dist/esm/recorders/observability/fileRecordingSink.d.ts +104 -0
- package/dist/esm/recorders/observability/fileRecordingSink.js +195 -0
- package/dist/esm/recorders/observability/fileRecordingSink.js.map +1 -0
- package/dist/esm/recorders/observability/recordingEnvelope.d.ts +292 -0
- package/dist/esm/recorders/observability/recordingEnvelope.js +375 -0
- package/dist/esm/recorders/observability/recordingEnvelope.js.map +1 -0
- package/dist/hosting/conformance/cases.js +23 -4
- package/dist/hosting/conformance/cases.js.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/observe.js +23 -1
- package/dist/observe.js.map +1 -1
- package/dist/recipes/apply.js +125 -0
- package/dist/recipes/apply.js.map +1 -0
- package/dist/recipes/defineAgentRecipe.js +118 -0
- package/dist/recipes/defineAgentRecipe.js.map +1 -0
- package/dist/recipes/identifier.js +100 -0
- package/dist/recipes/identifier.js.map +1 -0
- package/dist/recipes/index.js +24 -0
- package/dist/recipes/index.js.map +1 -0
- package/dist/recipes/provenance.js +57 -0
- package/dist/recipes/provenance.js.map +1 -0
- package/dist/recipes/types.js +64 -0
- package/dist/recipes/types.js.map +1 -0
- package/dist/recipes/version.js +89 -0
- package/dist/recipes/version.js.map +1 -0
- package/dist/recorders/observability/fileRecordingSink.js +201 -0
- package/dist/recorders/observability/fileRecordingSink.js.map +1 -0
- package/dist/recorders/observability/recordingEnvelope.js +382 -0
- package/dist/recorders/observability/recordingEnvelope.js.map +1 -0
- package/dist/types/adapters/code/agentcore.d.ts.map +1 -1
- package/dist/types/adapters/code/local.d.ts.map +1 -1
- package/dist/types/core/Agent.d.ts +9 -1
- package/dist/types/core/Agent.d.ts.map +1 -1
- package/dist/types/core/agent/AgentBuilder.d.ts +68 -0
- package/dist/types/core/agent/AgentBuilder.d.ts.map +1 -1
- package/dist/types/core/agent/runManifest.d.ts +18 -0
- package/dist/types/core/agent/runManifest.d.ts.map +1 -1
- package/dist/types/core/checkin.d.ts +58 -0
- package/dist/types/core/checkin.d.ts.map +1 -1
- package/dist/types/core/pause.d.ts +44 -0
- package/dist/types/core/pause.d.ts.map +1 -1
- package/dist/types/doors/recipes.d.ts +39 -0
- package/dist/types/doors/recipes.d.ts.map +1 -0
- package/dist/types/events/payloads.d.ts +29 -0
- package/dist/types/events/payloads.d.ts.map +1 -1
- package/dist/types/hosting/conformance/cases.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/observe.d.ts +2 -0
- package/dist/types/observe.d.ts.map +1 -1
- package/dist/types/recipes/apply.d.ts +68 -0
- package/dist/types/recipes/apply.d.ts.map +1 -0
- package/dist/types/recipes/defineAgentRecipe.d.ts +69 -0
- package/dist/types/recipes/defineAgentRecipe.d.ts.map +1 -0
- package/dist/types/recipes/identifier.d.ts +41 -0
- package/dist/types/recipes/identifier.d.ts.map +1 -0
- package/dist/types/recipes/index.d.ts +20 -0
- package/dist/types/recipes/index.d.ts.map +1 -0
- package/dist/types/recipes/provenance.d.ts +58 -0
- package/dist/types/recipes/provenance.d.ts.map +1 -0
- package/dist/types/recipes/types.d.ts +135 -0
- package/dist/types/recipes/types.d.ts.map +1 -0
- package/dist/types/recipes/version.d.ts +39 -0
- package/dist/types/recipes/version.d.ts.map +1 -0
- package/dist/types/recorders/observability/fileRecordingSink.d.ts +105 -0
- package/dist/types/recorders/observability/fileRecordingSink.d.ts.map +1 -0
- package/dist/types/recorders/observability/recordingEnvelope.d.ts +293 -0
- package/dist/types/recorders/observability/recordingEnvelope.d.ts.map +1 -0
- package/package.json +15 -2
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* version — is this string a version, or something that looks like one?
|
|
4
|
+
*
|
|
5
|
+
* Pattern: a total predicate + a refusal sentence. Pure, no dependencies.
|
|
6
|
+
* Role: recipes/ layer. Used by `defineAgentRecipe` and by the same
|
|
7
|
+
* validation `AgentBuilder.recipe()` runs on a hand-written literal.
|
|
8
|
+
* Emits: N/A.
|
|
9
|
+
*
|
|
10
|
+
* ## Why strict SemVer, and why a near-miss is refused rather than repaired
|
|
11
|
+
*
|
|
12
|
+
* The version is the half of a recipe row that makes the other half worth
|
|
13
|
+
* reading: two runs of `support-desk` that behaved differently are a mystery
|
|
14
|
+
* until the record says `1.2.0` and `1.3.0`. That only works if every producer
|
|
15
|
+
* spells versions the same way, so the ones that would quietly break grouping
|
|
16
|
+
* are refused at the declaration:
|
|
17
|
+
*
|
|
18
|
+
* `'1.2'` two runs, `'1.2'` and `'1.2.0'`, that are the same version and
|
|
19
|
+
* do not group together.
|
|
20
|
+
* `'v1.2.3'` the same, with a decoration that sorts differently.
|
|
21
|
+
* `'1.02.3'` leading zeros: `'1.02.3'` and `'1.2.3'` again split one arm.
|
|
22
|
+
* `'latest'` a RANGE, not a version — it names whatever was installed, so
|
|
23
|
+
* `'^1.2.3'` the row would describe a different composition each week and
|
|
24
|
+
* `'1.x'` say nothing about the run it is stamped on.
|
|
25
|
+
*
|
|
26
|
+
* None is repaired. Padding `'1.2'` to `'1.2.0'` would put a version on the
|
|
27
|
+
* record that the author never wrote, which is the one thing a manifest field
|
|
28
|
+
* may not do.
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.versionRefusal = exports.isSemverVersion = void 0;
|
|
32
|
+
/**
|
|
33
|
+
* The official SemVer 2.0.0 grammar (semver.org, "Backus–Naur Form Grammar for
|
|
34
|
+
* Valid SemVer Versions"), transcribed. Kept verbatim rather than loosened: a
|
|
35
|
+
* home-grown `\d+\.\d+\.\d+` would accept `01.2.3` and reject `1.0.0-rc.1`,
|
|
36
|
+
* which is wrong in both directions.
|
|
37
|
+
*/
|
|
38
|
+
const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
39
|
+
/** Whether `value` is a SemVer 2.0.0 version string. Total: any input, no throw. */
|
|
40
|
+
function isSemverVersion(value) {
|
|
41
|
+
return typeof value === 'string' && SEMVER.test(value);
|
|
42
|
+
}
|
|
43
|
+
exports.isSemverVersion = isSemverVersion;
|
|
44
|
+
/**
|
|
45
|
+
* The sentence a bad version gets. Names the value, the grammar, and the
|
|
46
|
+
* specific mistake when the shape identifies one — a refusal that only says
|
|
47
|
+
* "invalid" makes the author guess which half was wrong.
|
|
48
|
+
*
|
|
49
|
+
* @param callSite - the API the author called, e.g. `defineAgentRecipe`.
|
|
50
|
+
*/
|
|
51
|
+
function versionRefusal(callSite, value) {
|
|
52
|
+
const shown = typeof value === 'string' ? `'${value}'` : `${typeof value}`;
|
|
53
|
+
return (`${callSite}: version ${shown} is not a version. A recipe's version must be SemVer 2.0.0 ` +
|
|
54
|
+
`— three dot-separated numbers, optionally a prerelease and build ('1.2.0', '2.0.0-rc.1', ` +
|
|
55
|
+
`'1.0.0+build.5').\n\n` +
|
|
56
|
+
`${diagnose(value)}\n\n` +
|
|
57
|
+
`Nothing is padded or stripped for you: the version is stamped on the run manifest, where ` +
|
|
58
|
+
`it is the field two runs are grouped by, and a value the author never wrote would group ` +
|
|
59
|
+
`runs that are not the same composition.`);
|
|
60
|
+
}
|
|
61
|
+
exports.versionRefusal = versionRefusal;
|
|
62
|
+
/** The specific mistake, when the shape names one. Falls back to the general rule. */
|
|
63
|
+
function diagnose(value) {
|
|
64
|
+
if (typeof value !== 'string') {
|
|
65
|
+
return `A version is a string; this was ${value === null ? 'null' : typeof value}.`;
|
|
66
|
+
}
|
|
67
|
+
if (value.trim() === '')
|
|
68
|
+
return 'This one is empty.';
|
|
69
|
+
if (/^v/i.test(value)) {
|
|
70
|
+
return `Drop the leading '${value[0] ?? 'v'}' — SemVer carries no prefix ('1.2.0', not '${value}').`;
|
|
71
|
+
}
|
|
72
|
+
if (/^\d+\.\d+$/.test(value)) {
|
|
73
|
+
return (`This has two parts; SemVer has three. Did you mean '${value}.0'? Write it out — ` +
|
|
74
|
+
`'${value}' and '${value}.0' would be two labels for one composition.`);
|
|
75
|
+
}
|
|
76
|
+
if (/^\d+$/.test(value)) {
|
|
77
|
+
return `This is one number. Did you mean '${value}.0.0'?`;
|
|
78
|
+
}
|
|
79
|
+
if (/^[~^><=]/.test(value) || /\bx\b|\*/.test(value)) {
|
|
80
|
+
return (`This is a RANGE, not a version. A range names whatever happens to be installed, so the ` +
|
|
81
|
+
`manifest row would describe a different composition each week. Write the version this ` +
|
|
82
|
+
`recipe IS.`);
|
|
83
|
+
}
|
|
84
|
+
if (/^0\d|\.0\d/.test(value)) {
|
|
85
|
+
return `Numeric parts carry no leading zeros ('1.2.0', not '01.02.00').`;
|
|
86
|
+
}
|
|
87
|
+
return `Neither the numbers, the prerelease nor the build metadata matched the grammar.`;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/recipes/version.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;;;AAEH;;;;;GAKG;AACH,MAAM,MAAM,GACV,qLAAqL,CAAC;AAExL,oFAAoF;AACpF,SAAgB,eAAe,CAAC,KAAc;IAC5C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACzD,CAAC;AAFD,0CAEC;AAED;;;;;;GAMG;AACH,SAAgB,cAAc,CAAC,QAAgB,EAAE,KAAc;IAC7D,MAAM,KAAK,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,OAAO,KAAK,EAAE,CAAC;IAC3E,OAAO,CACL,GAAG,QAAQ,aAAa,KAAK,6DAA6D;QAC1F,2FAA2F;QAC3F,uBAAuB;QACvB,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM;QACxB,2FAA2F;QAC3F,0FAA0F;QAC1F,yCAAyC,CAC1C,CAAC;AACJ,CAAC;AAXD,wCAWC;AAED,sFAAsF;AACtF,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,mCAAmC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,KAAK,GAAG,CAAC;IACtF,CAAC;IACD,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,oBAAoB,CAAC;IACrD,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,qBACL,KAAK,CAAC,CAAC,CAAC,IAAI,GACd,+CAA+C,KAAK,KAAK,CAAC;IAC5D,CAAC;IACD,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,CACL,uDAAuD,KAAK,sBAAsB;YAClF,IAAI,KAAK,UAAU,KAAK,8CAA8C,CACvE,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,qCAAqC,KAAK,QAAQ,CAAC;IAC5D,CAAC;IACD,IAAI,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACrD,OAAO,CACL,yFAAyF;YACzF,wFAAwF;YACxF,YAAY,CACb,CAAC;IACJ,CAAC;IACD,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,iEAAiE,CAAC;IAC3E,CAAC;IACD,OAAO,iFAAiF,CAAC;AAC3F,CAAC"}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* fileRecordingSink — one archived run, one JSON file, in a directory.
|
|
4
|
+
*
|
|
5
|
+
* The destination that needs nothing installed: an incident can be inspected
|
|
6
|
+
* with `ls` and `cat`, and a run archive is a folder you can tar. It is also
|
|
7
|
+
* the reference implementation of {@link RecordingSink} — a sink is one method,
|
|
8
|
+
* and this file is what "implement the other ones like this" points at.
|
|
9
|
+
*
|
|
10
|
+
* ## The file name is a key, and keys must be injective
|
|
11
|
+
*
|
|
12
|
+
* `runId` becomes a file name, which makes `runId ↦ name` a mapping used as a
|
|
13
|
+
* key: two different runs landing on one name means one archive silently
|
|
14
|
+
* overwrites another, and the evidence is gone with no error anywhere. So the
|
|
15
|
+
* mapping is `runId + '.json'` — appending a constant suffix, which is
|
|
16
|
+
* injective — over a DOMAIN that is asserted rather than assumed, and anything
|
|
17
|
+
* outside it is refused by name.
|
|
18
|
+
*
|
|
19
|
+
* The domain is `[a-z0-9]` followed by `[a-z0-9._-]*`, and every exclusion is
|
|
20
|
+
* load-bearing:
|
|
21
|
+
*
|
|
22
|
+
* • **no uppercase.** This is the one that looks like fussiness and is not.
|
|
23
|
+
* macOS/APFS and Windows/NTFS are case-INSENSITIVE by default, so `run-A`
|
|
24
|
+
* and `run-a` are two distinct strings that name ONE file. A mapping that
|
|
25
|
+
* is injective as a string can still collide as a file name, which is
|
|
26
|
+
* exactly the bug artifacts/scopePath.ts found on a stock Mac in 9.44.0 —
|
|
27
|
+
* its conformance battery had pairs for separators, absence markers and
|
|
28
|
+
* pre-escaped values, and no pair differing only in case. Excluding
|
|
29
|
+
* uppercase from the domain means no two valid ids differ by case alone, so
|
|
30
|
+
* there is nothing for a case-folding filesystem to fold together.
|
|
31
|
+
* • **no `/` or `\`.** A separator inside a field is separator donation: an
|
|
32
|
+
* id containing one would silently become a path with a directory hop.
|
|
33
|
+
* • **no leading `.` or `-`.** A leading dot hides the archive from `ls`; a
|
|
34
|
+
* leading dash is read as a flag by every CLI tool that would then handle
|
|
35
|
+
* it. Both are enforced by the first-character class.
|
|
36
|
+
* • **no bare `.` / `..`.** Path navigation, not names.
|
|
37
|
+
* • **length capped.** `NAME_MAX` is 255 on the common filesystems and the
|
|
38
|
+
* suffixes here add to it.
|
|
39
|
+
* • **no Windows reserved device names.** `con`, `nul`, `com1` … are devices
|
|
40
|
+
* with OR without an extension: `con.json` opens the console, not a file.
|
|
41
|
+
*
|
|
42
|
+
* Ids this library mints all satisfy it: `makeRunId()` produces
|
|
43
|
+
* `run-<epoch ms>-<seq>` and the footprintjs engine produces
|
|
44
|
+
* `<epoch ms>-<padded counter>`. The assertion is there for the ids a CALLER
|
|
45
|
+
* states, which is the case that is neither controlled nor rare.
|
|
46
|
+
*
|
|
47
|
+
* ## Atomic, so a reader never sees half an archive
|
|
48
|
+
*
|
|
49
|
+
* The bytes go to a temporary file in the same directory and are then renamed
|
|
50
|
+
* into place. `rename` within one filesystem is atomic, so a crash mid-write
|
|
51
|
+
* leaves a `.tmp` nobody reads rather than a truncated `.json` that parses as
|
|
52
|
+
* far as it got — a half-written archive is the one failure a bug report cannot
|
|
53
|
+
* survive, because it looks like evidence.
|
|
54
|
+
*
|
|
55
|
+
* Writing the same `runId` twice REPLACES the file, atomically. That is the
|
|
56
|
+
* intended behaviour and worth stating: the run id is the archive's identity,
|
|
57
|
+
* so a second envelope for one run is a newer version of one archive (a partial
|
|
58
|
+
* crash dump later superseded by the finished run), not a second archive.
|
|
59
|
+
*
|
|
60
|
+
* Node-only. `node:fs` is reached through `lazyRequire`, the same law the other
|
|
61
|
+
* filesystem adapters follow, so importing the door costs a browser bundle
|
|
62
|
+
* nothing and constructing one where there is no filesystem refuses by name.
|
|
63
|
+
*/
|
|
64
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
65
|
+
exports.fileRecordingSink = exports.recordingFileName = exports.UnsafeRecordingIdError = void 0;
|
|
66
|
+
const lazyRequire_js_1 = require("../../lib/lazyRequire.js");
|
|
67
|
+
/** The archive file's extension. Also the tmp file's discriminator. */
|
|
68
|
+
const ENVELOPE_EXTENSION = '.json';
|
|
69
|
+
/** `NAME_MAX` is 255; leave room for the extension and the tmp suffix. */
|
|
70
|
+
const MAX_RUN_ID_LENGTH = 200;
|
|
71
|
+
/** Valid from the first character on — see the module header for each rule. */
|
|
72
|
+
const SAFE_RUN_ID = /^[a-z0-9][a-z0-9._-]*$/;
|
|
73
|
+
/**
|
|
74
|
+
* Reserved on Windows with any extension, so `con.json` is still the console.
|
|
75
|
+
* Checked against the whole id because the id IS the name's stem.
|
|
76
|
+
*/
|
|
77
|
+
const WINDOWS_RESERVED = new Set([
|
|
78
|
+
'con',
|
|
79
|
+
'prn',
|
|
80
|
+
'aux',
|
|
81
|
+
'nul',
|
|
82
|
+
...Array.from({ length: 9 }, (_, i) => `com${String(i + 1)}`),
|
|
83
|
+
...Array.from({ length: 9 }, (_, i) => `lpt${String(i + 1)}`),
|
|
84
|
+
]);
|
|
85
|
+
/**
|
|
86
|
+
* Raised when a run id cannot safely become a file name.
|
|
87
|
+
*
|
|
88
|
+
* Its own class because the fix is never a retry: the caller has to name the
|
|
89
|
+
* archive something a filesystem can hold one-to-one.
|
|
90
|
+
*/
|
|
91
|
+
class UnsafeRecordingIdError extends Error {
|
|
92
|
+
code = 'ERR_UNSAFE_RECORDING_ID';
|
|
93
|
+
runId;
|
|
94
|
+
constructor(runId, reason) {
|
|
95
|
+
super(`[recording-sink] run id ${JSON.stringify(runId)} cannot become a file name: ${reason}\n\n` +
|
|
96
|
+
`A file name is a key: if two runs can land on one name, one archive overwrites the ` +
|
|
97
|
+
`other and the evidence is gone with nothing raised anywhere. So the safe set is ` +
|
|
98
|
+
`asserted rather than hoped for — lowercase letters, digits, '.', '_' and '-', ` +
|
|
99
|
+
`starting with a letter or digit, at most ${String(MAX_RUN_ID_LENGTH)} characters. ` +
|
|
100
|
+
`(Uppercase is excluded because macOS and Windows fold case: 'run-A' and 'run-a' are ` +
|
|
101
|
+
`two ids and one file.) Either state a run.runId in that set, or write to a sink whose ` +
|
|
102
|
+
`keys are not file names.`);
|
|
103
|
+
this.name = 'UnsafeRecordingIdError';
|
|
104
|
+
this.runId = runId;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
exports.UnsafeRecordingIdError = UnsafeRecordingIdError;
|
|
108
|
+
/**
|
|
109
|
+
* The mapping under test: one run id → one file name, injectively.
|
|
110
|
+
*
|
|
111
|
+
* Exported so the collision battery can drive the mapping directly rather than
|
|
112
|
+
* inferring it from files on a disk.
|
|
113
|
+
*
|
|
114
|
+
* @throws {UnsafeRecordingIdError} for any id outside the safe domain.
|
|
115
|
+
*/
|
|
116
|
+
function recordingFileName(runId) {
|
|
117
|
+
if (typeof runId !== 'string' || runId === '') {
|
|
118
|
+
throw new UnsafeRecordingIdError(String(runId), 'it is not a non-empty string.');
|
|
119
|
+
}
|
|
120
|
+
if (runId.length > MAX_RUN_ID_LENGTH) {
|
|
121
|
+
throw new UnsafeRecordingIdError(runId, `it is ${String(runId.length)} characters; the limit is ${String(MAX_RUN_ID_LENGTH)}.`);
|
|
122
|
+
}
|
|
123
|
+
if (/[A-Z]/.test(runId)) {
|
|
124
|
+
throw new UnsafeRecordingIdError(runId, 'it contains uppercase letters, and case-insensitive filesystems (macOS, Windows) would ' +
|
|
125
|
+
'let it share a file with its lowercase twin.');
|
|
126
|
+
}
|
|
127
|
+
if (!SAFE_RUN_ID.test(runId)) {
|
|
128
|
+
throw new UnsafeRecordingIdError(runId, 'it must start with a lowercase letter or digit and contain only [a-z0-9._-] — path ' +
|
|
129
|
+
'separators, spaces and control characters are not names.');
|
|
130
|
+
}
|
|
131
|
+
if (/^\.+$/.test(runId)) {
|
|
132
|
+
throw new UnsafeRecordingIdError(runId, 'it is only dots, which is path navigation.');
|
|
133
|
+
}
|
|
134
|
+
if (WINDOWS_RESERVED.has(runId)) {
|
|
135
|
+
throw new UnsafeRecordingIdError(runId, `'${runId}' is a reserved device name on Windows, with or without an extension.`);
|
|
136
|
+
}
|
|
137
|
+
return `${runId}${ENVELOPE_EXTENSION}`;
|
|
138
|
+
}
|
|
139
|
+
exports.recordingFileName = recordingFileName;
|
|
140
|
+
/** Process-local, so two sinks in one process cannot pick one tmp name. */
|
|
141
|
+
let _tmpSeq = 0;
|
|
142
|
+
/**
|
|
143
|
+
* A directory-backed recording sink — one JSON file per run, written atomically.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```ts
|
|
147
|
+
* const recorder = recordRun(agent);
|
|
148
|
+
* await agent.run({ message: 'hi' });
|
|
149
|
+
*
|
|
150
|
+
* await persistRecording(recorder, {
|
|
151
|
+
* sink: fileRecordingSink({ directory: './run-archive' }),
|
|
152
|
+
* run: { complete: true },
|
|
153
|
+
* });
|
|
154
|
+
* // → ./run-archive/run-1787093273110-1.json
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
function fileRecordingSink(options) {
|
|
158
|
+
const directory = options?.directory;
|
|
159
|
+
if (typeof directory !== 'string' || directory.trim() === '') {
|
|
160
|
+
throw new TypeError(`[recording-sink] fileRecordingSink({ directory: ${JSON.stringify(directory)} }) names no ` +
|
|
161
|
+
`directory. Give it a path — it will be created if it does not exist.`);
|
|
162
|
+
}
|
|
163
|
+
const fs = (0, lazyRequire_js_1.lazyRequire)('node:fs');
|
|
164
|
+
const path = (0, lazyRequire_js_1.lazyRequire)('node:path');
|
|
165
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
166
|
+
return {
|
|
167
|
+
async write(envelope) {
|
|
168
|
+
// Assert the name BEFORE serializing: a refusal should cost nothing, and
|
|
169
|
+
// an id that cannot be filed should not first be turned into megabytes.
|
|
170
|
+
const name = recordingFileName(envelope?.run?.runId);
|
|
171
|
+
const target = path.join(directory, name);
|
|
172
|
+
let text;
|
|
173
|
+
try {
|
|
174
|
+
text = `${JSON.stringify(envelope, null, 2)}\n`;
|
|
175
|
+
}
|
|
176
|
+
catch (cause) {
|
|
177
|
+
// A circular reference or a BigInt inside the recording. Refuse by
|
|
178
|
+
// name: the alternative is a file that exists and is not the run.
|
|
179
|
+
throw new TypeError(`[recording-sink] this recording cannot be written as JSON: ${cause instanceof Error ? cause.message : String(cause)}. A run's state must survive structuredClone to be recorded at all, so this ` +
|
|
180
|
+
`normally means a value was put into state after the run (a circular object, a ` +
|
|
181
|
+
`BigInt). Nothing was written — a partial archive would be worse than none.`);
|
|
182
|
+
}
|
|
183
|
+
// Same directory, so the rename is a same-filesystem move and therefore
|
|
184
|
+
// atomic. `.tmp` can never collide with an archive name: archives always
|
|
185
|
+
// end in `.json`, these always end in `.tmp`.
|
|
186
|
+
const tmp = path.join(directory, `${name}.${String(process.pid)}.${String(++_tmpSeq)}.tmp`);
|
|
187
|
+
try {
|
|
188
|
+
await fs.promises.writeFile(tmp, text, 'utf8');
|
|
189
|
+
await fs.promises.rename(tmp, target);
|
|
190
|
+
}
|
|
191
|
+
catch (cause) {
|
|
192
|
+
// Leave no half-written debris behind, but never mask the real error.
|
|
193
|
+
await fs.promises.rm(tmp, { force: true }).catch(() => undefined);
|
|
194
|
+
throw cause;
|
|
195
|
+
}
|
|
196
|
+
return { id: name, uri: `file://${path.resolve(target)}` };
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
exports.fileRecordingSink = fileRecordingSink;
|
|
201
|
+
//# sourceMappingURL=fileRecordingSink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileRecordingSink.js","sourceRoot":"","sources":["../../../src/recorders/observability/fileRecordingSink.ts"],"names":[],"mappings":"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"}
|