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,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provenance — who registered this name.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: a small value type + one formatter. Pure, no dependencies beyond
|
|
5
|
+
* the recipe types.
|
|
6
|
+
* Role: recipes/ layer. `AgentBuilder` records a {@link RecipeSource} beside
|
|
7
|
+
* every tool name and injection id it takes, and reads it back when
|
|
8
|
+
* two registrations collide.
|
|
9
|
+
* Emits: N/A.
|
|
10
|
+
*
|
|
11
|
+
* ## Why provenance at all
|
|
12
|
+
*
|
|
13
|
+
* The builder has always refused a duplicate tool name — the model dispatches
|
|
14
|
+
* by name, so two tools called `search` is a coin flip. What it could not say
|
|
15
|
+
* was WHERE each one came from. With one recipe in the chain that answer stops
|
|
16
|
+
* being obvious ("I never registered a `search` tool") and with two it is
|
|
17
|
+
* unrecoverable without reading both recipes' source. A refusal that names both
|
|
18
|
+
* sides turns a hunt into a sentence.
|
|
19
|
+
*
|
|
20
|
+
* ## Three arms, because three things are true and one of them is "I do not know"
|
|
21
|
+
*
|
|
22
|
+
* `local` and `unattributed` are NOT the same fact, and collapsing them would
|
|
23
|
+
* make the refusal lie. `local` means: this builder watched you call
|
|
24
|
+
* `.tool()` / `.injection()` yourself. `unattributed` means: the name was
|
|
25
|
+
* already taken when the ledger was consulted and no source was recorded for
|
|
26
|
+
* it — the honest answer is that this builder does not know, and saying
|
|
27
|
+
* "you registered it" would send the reader to look in the one place it is not.
|
|
28
|
+
*
|
|
29
|
+
* The arm exists because there is more than one way into the injection list:
|
|
30
|
+
* `.injection()` is the funnel for the named flavors, and `.outputSchema()`
|
|
31
|
+
* mounts an instruction of its own. Both record a source today. A third site
|
|
32
|
+
* added later would not, and this arm is what keeps that omission honest
|
|
33
|
+
* instead of blaming the caller.
|
|
34
|
+
*/
|
|
35
|
+
/** The one `local` value. A frozen singleton — it carries no data. */
|
|
36
|
+
export const LOCAL_SOURCE = Object.freeze({ kind: 'local' });
|
|
37
|
+
/**
|
|
38
|
+
* Describe a source in a sentence fragment that reads inside a refusal.
|
|
39
|
+
*
|
|
40
|
+
* `undefined` is the unattributed case — see the header. It is a separate
|
|
41
|
+
* answer, never folded into `local`.
|
|
42
|
+
*/
|
|
43
|
+
export function describeRecipeSource(source) {
|
|
44
|
+
if (source === undefined) {
|
|
45
|
+
return ('an earlier registration with no recorded source (a builder method mounted it; ' +
|
|
46
|
+
'this builder did not attribute it)');
|
|
47
|
+
}
|
|
48
|
+
if (source.kind === 'local')
|
|
49
|
+
return 'this agent, directly';
|
|
50
|
+
const chain = [...source.stack].reverse().map((r) => `recipe '${r.id}' ${r.version}`);
|
|
51
|
+
return chain.join(' ← applied by ');
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=provenance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provenance.js","sourceRoot":"","sources":["../../../src/recipes/provenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAeH,sEAAsE;AACtE,MAAM,CAAC,MAAM,YAAY,GAAiB,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,OAAgB,EAAE,CAAC,CAAC;AAEpF;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAgC;IACnE,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,CACL,gFAAgF;YAChF,oCAAoC,CACrC,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,sBAAsB,CAAC;IAC3D,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;IACtF,OAAO,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;AACtC,CAAC"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recipes/types — the declared unit of agent configuration.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: a data DECLARATION over an existing fluent builder. No class, no
|
|
5
|
+
* registry, no lifecycle. The repo's own instruction is that named
|
|
6
|
+
* patterns are recipes over primitives rather than new machinery, and
|
|
7
|
+
* this file is that instruction taken literally.
|
|
8
|
+
* Role: recipes/ layer, pure. Nothing here imports the engine, a provider,
|
|
9
|
+
* or a store — the only agentfootprint type it names is
|
|
10
|
+
* {@link AgentBuilder}, and it names it as a TYPE.
|
|
11
|
+
* Emits: N/A. `AgentBuilder.recipe()` records the row; `runManifest` reports
|
|
12
|
+
* it.
|
|
13
|
+
*
|
|
14
|
+
* ## The gap this closes
|
|
15
|
+
*
|
|
16
|
+
* Every capability an agent needs already ships. What did not ship was a
|
|
17
|
+
* declared, versioned, inspectable unit of CONFIGURATION. So an agent's setup
|
|
18
|
+
* lived as prose in an example, was copy-pasted into an app, drifted there, and
|
|
19
|
+
* afterwards nothing on the run could say which composition produced the agent
|
|
20
|
+
* that answered. Two runs of "the support agent" could differ in every tool and
|
|
21
|
+
* every instruction and be indistinguishable on the record.
|
|
22
|
+
*
|
|
23
|
+
* A recipe is the missing noun: a name, a version, and a function that calls
|
|
24
|
+
* the builder methods that already exist.
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* export const supportDesk = defineAgentRecipe({
|
|
28
|
+
* id: 'support-desk',
|
|
29
|
+
* version: '1.2.0',
|
|
30
|
+
* description: 'Order lookup + refund policy, the way support runs it.',
|
|
31
|
+
* configure: (agent) => {
|
|
32
|
+
* agent.system('You answer support questions.').tool(lookupOrder);
|
|
33
|
+
* },
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* const agent = Agent.create({ provider, model }).recipe(supportDesk).build();
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* ## No lifecycle magic — stated here because it is the load-bearing limit
|
|
40
|
+
*
|
|
41
|
+
* A recipe composes CONFIGURATION and nothing else:
|
|
42
|
+
*
|
|
43
|
+
* • nothing is registered anywhere — a recipe is not discovered, not looked
|
|
44
|
+
* up by name, and not resolved from a registry. You import the object and
|
|
45
|
+
* hand it to `.recipe()`. There is no global map to go stale, and no
|
|
46
|
+
* "which version of `support-desk` is installed" question;
|
|
47
|
+
* • nothing is closable — a recipe holds no connection, no handle and no
|
|
48
|
+
* process. It never gets a `close()`, a `dispose()` or a teardown hook,
|
|
49
|
+
* and it is never awaited: `configure` is synchronous because `build()`
|
|
50
|
+
* is (an `async configure` is refused by name rather than silently not
|
|
51
|
+
* awaited);
|
|
52
|
+
* • nothing is deferred — every call `configure` makes happens during
|
|
53
|
+
* `.recipe()`, at the position in the chain where you wrote it. There is
|
|
54
|
+
* no later phase in which a recipe acts, so a run's behaviour is decided
|
|
55
|
+
* entirely by the builder calls you can read.
|
|
56
|
+
*
|
|
57
|
+
* That is deliberately thin. The thing being named is a COMPOSITION, and a
|
|
58
|
+
* composition that also owned a resource would be a component wearing a
|
|
59
|
+
* composition's name — the shape that makes "who closed the pool?" unanswerable
|
|
60
|
+
* two releases later.
|
|
61
|
+
*/
|
|
62
|
+
import type { AgentBuilder } from '../core/agent/AgentBuilder.js';
|
|
63
|
+
/**
|
|
64
|
+
* A named, versioned composition over the agent builder.
|
|
65
|
+
*
|
|
66
|
+
* Build one with {@link defineAgentRecipe}, which validates every field and
|
|
67
|
+
* freezes the result. A hand-written object literal is accepted by
|
|
68
|
+
* `.recipe()` too — it runs the SAME validation — so a recipe cannot reach an
|
|
69
|
+
* agent without passing the checks; the factory only moves the refusal to the
|
|
70
|
+
* declaration, where the fix is.
|
|
71
|
+
*/
|
|
72
|
+
export interface AgentRecipe {
|
|
73
|
+
/**
|
|
74
|
+
* The composition's plain name — lower-case words joined by single hyphens
|
|
75
|
+
* (`support-desk`, `triage`, `refund-policy`).
|
|
76
|
+
*
|
|
77
|
+
* It carries NO version: `support-desk-2` is refused, because the version
|
|
78
|
+
* axis already exists as its own field and an id that encodes one produces
|
|
79
|
+
* two names for one composition, neither of which can be grouped on.
|
|
80
|
+
*/
|
|
81
|
+
readonly id: string;
|
|
82
|
+
/**
|
|
83
|
+
* The composition's version, as SemVer 2.0.0 (`'1.2.0'`, `'2.0.0-rc.1'`).
|
|
84
|
+
*
|
|
85
|
+
* A version is what makes a recipe row on a run manifest worth reading: two
|
|
86
|
+
* runs of `support-desk` that answered differently are a mystery until the
|
|
87
|
+
* record says one was `1.2.0` and the other `1.3.0`.
|
|
88
|
+
*/
|
|
89
|
+
readonly version: string;
|
|
90
|
+
/** What this composition is for, in one sentence. Optional, and never
|
|
91
|
+
* reported on the wire — the manifest carries the id and the version only. */
|
|
92
|
+
readonly description?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Apply the composition: call the builder methods this recipe stands for.
|
|
95
|
+
*
|
|
96
|
+
* Runs SYNCHRONOUSLY, exactly once, at the `.recipe()` call site. The return
|
|
97
|
+
* value is ignored — `AgentBuilder` is mutated in place and every method
|
|
98
|
+
* returns the same object — so `(agent) => agent.system('…').tool(t)` and a
|
|
99
|
+
* statement body are the same program.
|
|
100
|
+
*/
|
|
101
|
+
configure(builder: AgentBuilder): void;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* One applied recipe, as the run manifest reports it: the id and the version,
|
|
105
|
+
* never the description and never the function.
|
|
106
|
+
*
|
|
107
|
+
* The two stay SEPARATE FIELDS on purpose. A composed key (`'support-desk@1.2.0'`)
|
|
108
|
+
* would be one string that two different pairs could produce as soon as either
|
|
109
|
+
* half is allowed to contain the separator — the collision class this repo has
|
|
110
|
+
* fixed seven times. Nothing here ever joins them.
|
|
111
|
+
*/
|
|
112
|
+
export interface AppliedRecipe {
|
|
113
|
+
readonly id: string;
|
|
114
|
+
readonly version: string;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* What happens when a recipe introduces a tool name or an injection id that is
|
|
118
|
+
* already taken.
|
|
119
|
+
*
|
|
120
|
+
* `'error'` is the only policy that exists, and it is the default. The
|
|
121
|
+
* alternatives a reader will reach for — skip the recipe's version, let it
|
|
122
|
+
* replace what is there, rename one automatically — are real designs, and none
|
|
123
|
+
* of them is implemented: each has to answer where the drop is RECORDED, and a
|
|
124
|
+
* conflict resolved silently is exactly the "accepted and quietly wrong" shape
|
|
125
|
+
* this library refuses. So an unimplemented policy is refused by name (see
|
|
126
|
+
* {@link resolveRecipeConflictPolicy}) rather than approximated by the one that
|
|
127
|
+
* ships.
|
|
128
|
+
*/
|
|
129
|
+
export type RecipeConflictPolicy = 'error';
|
|
130
|
+
/** Options for `AgentBuilder.recipe(recipe, options)`. */
|
|
131
|
+
export interface RecipeOptions {
|
|
132
|
+
/** See {@link RecipeConflictPolicy}. Default `'error'`. */
|
|
133
|
+
readonly conflict?: RecipeConflictPolicy;
|
|
134
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recipes/types — the declared unit of agent configuration.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: a data DECLARATION over an existing fluent builder. No class, no
|
|
5
|
+
* registry, no lifecycle. The repo's own instruction is that named
|
|
6
|
+
* patterns are recipes over primitives rather than new machinery, and
|
|
7
|
+
* this file is that instruction taken literally.
|
|
8
|
+
* Role: recipes/ layer, pure. Nothing here imports the engine, a provider,
|
|
9
|
+
* or a store — the only agentfootprint type it names is
|
|
10
|
+
* {@link AgentBuilder}, and it names it as a TYPE.
|
|
11
|
+
* Emits: N/A. `AgentBuilder.recipe()` records the row; `runManifest` reports
|
|
12
|
+
* it.
|
|
13
|
+
*
|
|
14
|
+
* ## The gap this closes
|
|
15
|
+
*
|
|
16
|
+
* Every capability an agent needs already ships. What did not ship was a
|
|
17
|
+
* declared, versioned, inspectable unit of CONFIGURATION. So an agent's setup
|
|
18
|
+
* lived as prose in an example, was copy-pasted into an app, drifted there, and
|
|
19
|
+
* afterwards nothing on the run could say which composition produced the agent
|
|
20
|
+
* that answered. Two runs of "the support agent" could differ in every tool and
|
|
21
|
+
* every instruction and be indistinguishable on the record.
|
|
22
|
+
*
|
|
23
|
+
* A recipe is the missing noun: a name, a version, and a function that calls
|
|
24
|
+
* the builder methods that already exist.
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* export const supportDesk = defineAgentRecipe({
|
|
28
|
+
* id: 'support-desk',
|
|
29
|
+
* version: '1.2.0',
|
|
30
|
+
* description: 'Order lookup + refund policy, the way support runs it.',
|
|
31
|
+
* configure: (agent) => {
|
|
32
|
+
* agent.system('You answer support questions.').tool(lookupOrder);
|
|
33
|
+
* },
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* const agent = Agent.create({ provider, model }).recipe(supportDesk).build();
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* ## No lifecycle magic — stated here because it is the load-bearing limit
|
|
40
|
+
*
|
|
41
|
+
* A recipe composes CONFIGURATION and nothing else:
|
|
42
|
+
*
|
|
43
|
+
* • nothing is registered anywhere — a recipe is not discovered, not looked
|
|
44
|
+
* up by name, and not resolved from a registry. You import the object and
|
|
45
|
+
* hand it to `.recipe()`. There is no global map to go stale, and no
|
|
46
|
+
* "which version of `support-desk` is installed" question;
|
|
47
|
+
* • nothing is closable — a recipe holds no connection, no handle and no
|
|
48
|
+
* process. It never gets a `close()`, a `dispose()` or a teardown hook,
|
|
49
|
+
* and it is never awaited: `configure` is synchronous because `build()`
|
|
50
|
+
* is (an `async configure` is refused by name rather than silently not
|
|
51
|
+
* awaited);
|
|
52
|
+
* • nothing is deferred — every call `configure` makes happens during
|
|
53
|
+
* `.recipe()`, at the position in the chain where you wrote it. There is
|
|
54
|
+
* no later phase in which a recipe acts, so a run's behaviour is decided
|
|
55
|
+
* entirely by the builder calls you can read.
|
|
56
|
+
*
|
|
57
|
+
* That is deliberately thin. The thing being named is a COMPOSITION, and a
|
|
58
|
+
* composition that also owned a resource would be a component wearing a
|
|
59
|
+
* composition's name — the shape that makes "who closed the pool?" unanswerable
|
|
60
|
+
* two releases later.
|
|
61
|
+
*/
|
|
62
|
+
export {};
|
|
63
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/recipes/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* version — is this string a version, or something that looks like one?
|
|
3
|
+
*
|
|
4
|
+
* Pattern: a total predicate + a refusal sentence. Pure, no dependencies.
|
|
5
|
+
* Role: recipes/ layer. Used by `defineAgentRecipe` and by the same
|
|
6
|
+
* validation `AgentBuilder.recipe()` runs on a hand-written literal.
|
|
7
|
+
* Emits: N/A.
|
|
8
|
+
*
|
|
9
|
+
* ## Why strict SemVer, and why a near-miss is refused rather than repaired
|
|
10
|
+
*
|
|
11
|
+
* The version is the half of a recipe row that makes the other half worth
|
|
12
|
+
* reading: two runs of `support-desk` that behaved differently are a mystery
|
|
13
|
+
* until the record says `1.2.0` and `1.3.0`. That only works if every producer
|
|
14
|
+
* spells versions the same way, so the ones that would quietly break grouping
|
|
15
|
+
* are refused at the declaration:
|
|
16
|
+
*
|
|
17
|
+
* `'1.2'` two runs, `'1.2'` and `'1.2.0'`, that are the same version and
|
|
18
|
+
* do not group together.
|
|
19
|
+
* `'v1.2.3'` the same, with a decoration that sorts differently.
|
|
20
|
+
* `'1.02.3'` leading zeros: `'1.02.3'` and `'1.2.3'` again split one arm.
|
|
21
|
+
* `'latest'` a RANGE, not a version — it names whatever was installed, so
|
|
22
|
+
* `'^1.2.3'` the row would describe a different composition each week and
|
|
23
|
+
* `'1.x'` say nothing about the run it is stamped on.
|
|
24
|
+
*
|
|
25
|
+
* None is repaired. Padding `'1.2'` to `'1.2.0'` would put a version on the
|
|
26
|
+
* record that the author never wrote, which is the one thing a manifest field
|
|
27
|
+
* may not do.
|
|
28
|
+
*/
|
|
29
|
+
/** Whether `value` is a SemVer 2.0.0 version string. Total: any input, no throw. */
|
|
30
|
+
export declare function isSemverVersion(value: unknown): value is string;
|
|
31
|
+
/**
|
|
32
|
+
* The sentence a bad version gets. Names the value, the grammar, and the
|
|
33
|
+
* specific mistake when the shape identifies one — a refusal that only says
|
|
34
|
+
* "invalid" makes the author guess which half was wrong.
|
|
35
|
+
*
|
|
36
|
+
* @param callSite - the API the author called, e.g. `defineAgentRecipe`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function versionRefusal(callSite: string, value: unknown): string;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* version — is this string a version, or something that looks like one?
|
|
3
|
+
*
|
|
4
|
+
* Pattern: a total predicate + a refusal sentence. Pure, no dependencies.
|
|
5
|
+
* Role: recipes/ layer. Used by `defineAgentRecipe` and by the same
|
|
6
|
+
* validation `AgentBuilder.recipe()` runs on a hand-written literal.
|
|
7
|
+
* Emits: N/A.
|
|
8
|
+
*
|
|
9
|
+
* ## Why strict SemVer, and why a near-miss is refused rather than repaired
|
|
10
|
+
*
|
|
11
|
+
* The version is the half of a recipe row that makes the other half worth
|
|
12
|
+
* reading: two runs of `support-desk` that behaved differently are a mystery
|
|
13
|
+
* until the record says `1.2.0` and `1.3.0`. That only works if every producer
|
|
14
|
+
* spells versions the same way, so the ones that would quietly break grouping
|
|
15
|
+
* are refused at the declaration:
|
|
16
|
+
*
|
|
17
|
+
* `'1.2'` two runs, `'1.2'` and `'1.2.0'`, that are the same version and
|
|
18
|
+
* do not group together.
|
|
19
|
+
* `'v1.2.3'` the same, with a decoration that sorts differently.
|
|
20
|
+
* `'1.02.3'` leading zeros: `'1.02.3'` and `'1.2.3'` again split one arm.
|
|
21
|
+
* `'latest'` a RANGE, not a version — it names whatever was installed, so
|
|
22
|
+
* `'^1.2.3'` the row would describe a different composition each week and
|
|
23
|
+
* `'1.x'` say nothing about the run it is stamped on.
|
|
24
|
+
*
|
|
25
|
+
* None is repaired. Padding `'1.2'` to `'1.2.0'` would put a version on the
|
|
26
|
+
* record that the author never wrote, which is the one thing a manifest field
|
|
27
|
+
* may not do.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* The official SemVer 2.0.0 grammar (semver.org, "Backus–Naur Form Grammar for
|
|
31
|
+
* Valid SemVer Versions"), transcribed. Kept verbatim rather than loosened: a
|
|
32
|
+
* home-grown `\d+\.\d+\.\d+` would accept `01.2.3` and reject `1.0.0-rc.1`,
|
|
33
|
+
* which is wrong in both directions.
|
|
34
|
+
*/
|
|
35
|
+
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-]+)*))?$/;
|
|
36
|
+
/** Whether `value` is a SemVer 2.0.0 version string. Total: any input, no throw. */
|
|
37
|
+
export function isSemverVersion(value) {
|
|
38
|
+
return typeof value === 'string' && SEMVER.test(value);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The sentence a bad version gets. Names the value, the grammar, and the
|
|
42
|
+
* specific mistake when the shape identifies one — a refusal that only says
|
|
43
|
+
* "invalid" makes the author guess which half was wrong.
|
|
44
|
+
*
|
|
45
|
+
* @param callSite - the API the author called, e.g. `defineAgentRecipe`.
|
|
46
|
+
*/
|
|
47
|
+
export function versionRefusal(callSite, value) {
|
|
48
|
+
const shown = typeof value === 'string' ? `'${value}'` : `${typeof value}`;
|
|
49
|
+
return (`${callSite}: version ${shown} is not a version. A recipe's version must be SemVer 2.0.0 ` +
|
|
50
|
+
`— three dot-separated numbers, optionally a prerelease and build ('1.2.0', '2.0.0-rc.1', ` +
|
|
51
|
+
`'1.0.0+build.5').\n\n` +
|
|
52
|
+
`${diagnose(value)}\n\n` +
|
|
53
|
+
`Nothing is padded or stripped for you: the version is stamped on the run manifest, where ` +
|
|
54
|
+
`it is the field two runs are grouped by, and a value the author never wrote would group ` +
|
|
55
|
+
`runs that are not the same composition.`);
|
|
56
|
+
}
|
|
57
|
+
/** The specific mistake, when the shape names one. Falls back to the general rule. */
|
|
58
|
+
function diagnose(value) {
|
|
59
|
+
if (typeof value !== 'string') {
|
|
60
|
+
return `A version is a string; this was ${value === null ? 'null' : typeof value}.`;
|
|
61
|
+
}
|
|
62
|
+
if (value.trim() === '')
|
|
63
|
+
return 'This one is empty.';
|
|
64
|
+
if (/^v/i.test(value)) {
|
|
65
|
+
return `Drop the leading '${value[0] ?? 'v'}' — SemVer carries no prefix ('1.2.0', not '${value}').`;
|
|
66
|
+
}
|
|
67
|
+
if (/^\d+\.\d+$/.test(value)) {
|
|
68
|
+
return (`This has two parts; SemVer has three. Did you mean '${value}.0'? Write it out — ` +
|
|
69
|
+
`'${value}' and '${value}.0' would be two labels for one composition.`);
|
|
70
|
+
}
|
|
71
|
+
if (/^\d+$/.test(value)) {
|
|
72
|
+
return `This is one number. Did you mean '${value}.0.0'?`;
|
|
73
|
+
}
|
|
74
|
+
if (/^[~^><=]/.test(value) || /\bx\b|\*/.test(value)) {
|
|
75
|
+
return (`This is a RANGE, not a version. A range names whatever happens to be installed, so the ` +
|
|
76
|
+
`manifest row would describe a different composition each week. Write the version this ` +
|
|
77
|
+
`recipe IS.`);
|
|
78
|
+
}
|
|
79
|
+
if (/^0\d|\.0\d/.test(value)) {
|
|
80
|
+
return `Numeric parts carry no leading zeros ('1.2.0', not '01.02.00').`;
|
|
81
|
+
}
|
|
82
|
+
return `Neither the numbers, the prerelease nor the build metadata matched the grammar.`;
|
|
83
|
+
}
|
|
84
|
+
//# 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,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACzD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,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;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,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fileRecordingSink — one archived run, one JSON file, in a directory.
|
|
3
|
+
*
|
|
4
|
+
* The destination that needs nothing installed: an incident can be inspected
|
|
5
|
+
* with `ls` and `cat`, and a run archive is a folder you can tar. It is also
|
|
6
|
+
* the reference implementation of {@link RecordingSink} — a sink is one method,
|
|
7
|
+
* and this file is what "implement the other ones like this" points at.
|
|
8
|
+
*
|
|
9
|
+
* ## The file name is a key, and keys must be injective
|
|
10
|
+
*
|
|
11
|
+
* `runId` becomes a file name, which makes `runId ↦ name` a mapping used as a
|
|
12
|
+
* key: two different runs landing on one name means one archive silently
|
|
13
|
+
* overwrites another, and the evidence is gone with no error anywhere. So the
|
|
14
|
+
* mapping is `runId + '.json'` — appending a constant suffix, which is
|
|
15
|
+
* injective — over a DOMAIN that is asserted rather than assumed, and anything
|
|
16
|
+
* outside it is refused by name.
|
|
17
|
+
*
|
|
18
|
+
* The domain is `[a-z0-9]` followed by `[a-z0-9._-]*`, and every exclusion is
|
|
19
|
+
* load-bearing:
|
|
20
|
+
*
|
|
21
|
+
* • **no uppercase.** This is the one that looks like fussiness and is not.
|
|
22
|
+
* macOS/APFS and Windows/NTFS are case-INSENSITIVE by default, so `run-A`
|
|
23
|
+
* and `run-a` are two distinct strings that name ONE file. A mapping that
|
|
24
|
+
* is injective as a string can still collide as a file name, which is
|
|
25
|
+
* exactly the bug artifacts/scopePath.ts found on a stock Mac in 9.44.0 —
|
|
26
|
+
* its conformance battery had pairs for separators, absence markers and
|
|
27
|
+
* pre-escaped values, and no pair differing only in case. Excluding
|
|
28
|
+
* uppercase from the domain means no two valid ids differ by case alone, so
|
|
29
|
+
* there is nothing for a case-folding filesystem to fold together.
|
|
30
|
+
* • **no `/` or `\`.** A separator inside a field is separator donation: an
|
|
31
|
+
* id containing one would silently become a path with a directory hop.
|
|
32
|
+
* • **no leading `.` or `-`.** A leading dot hides the archive from `ls`; a
|
|
33
|
+
* leading dash is read as a flag by every CLI tool that would then handle
|
|
34
|
+
* it. Both are enforced by the first-character class.
|
|
35
|
+
* • **no bare `.` / `..`.** Path navigation, not names.
|
|
36
|
+
* • **length capped.** `NAME_MAX` is 255 on the common filesystems and the
|
|
37
|
+
* suffixes here add to it.
|
|
38
|
+
* • **no Windows reserved device names.** `con`, `nul`, `com1` … are devices
|
|
39
|
+
* with OR without an extension: `con.json` opens the console, not a file.
|
|
40
|
+
*
|
|
41
|
+
* Ids this library mints all satisfy it: `makeRunId()` produces
|
|
42
|
+
* `run-<epoch ms>-<seq>` and the footprintjs engine produces
|
|
43
|
+
* `<epoch ms>-<padded counter>`. The assertion is there for the ids a CALLER
|
|
44
|
+
* states, which is the case that is neither controlled nor rare.
|
|
45
|
+
*
|
|
46
|
+
* ## Atomic, so a reader never sees half an archive
|
|
47
|
+
*
|
|
48
|
+
* The bytes go to a temporary file in the same directory and are then renamed
|
|
49
|
+
* into place. `rename` within one filesystem is atomic, so a crash mid-write
|
|
50
|
+
* leaves a `.tmp` nobody reads rather than a truncated `.json` that parses as
|
|
51
|
+
* far as it got — a half-written archive is the one failure a bug report cannot
|
|
52
|
+
* survive, because it looks like evidence.
|
|
53
|
+
*
|
|
54
|
+
* Writing the same `runId` twice REPLACES the file, atomically. That is the
|
|
55
|
+
* intended behaviour and worth stating: the run id is the archive's identity,
|
|
56
|
+
* so a second envelope for one run is a newer version of one archive (a partial
|
|
57
|
+
* crash dump later superseded by the finished run), not a second archive.
|
|
58
|
+
*
|
|
59
|
+
* Node-only. `node:fs` is reached through `lazyRequire`, the same law the other
|
|
60
|
+
* filesystem adapters follow, so importing the door costs a browser bundle
|
|
61
|
+
* nothing and constructing one where there is no filesystem refuses by name.
|
|
62
|
+
*/
|
|
63
|
+
import type { RecordingSink } from './recordingEnvelope.js';
|
|
64
|
+
/**
|
|
65
|
+
* Raised when a run id cannot safely become a file name.
|
|
66
|
+
*
|
|
67
|
+
* Its own class because the fix is never a retry: the caller has to name the
|
|
68
|
+
* archive something a filesystem can hold one-to-one.
|
|
69
|
+
*/
|
|
70
|
+
export declare class UnsafeRecordingIdError extends Error {
|
|
71
|
+
readonly code: "ERR_UNSAFE_RECORDING_ID";
|
|
72
|
+
readonly runId: string;
|
|
73
|
+
constructor(runId: string, reason: string);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The mapping under test: one run id → one file name, injectively.
|
|
77
|
+
*
|
|
78
|
+
* Exported so the collision battery can drive the mapping directly rather than
|
|
79
|
+
* inferring it from files on a disk.
|
|
80
|
+
*
|
|
81
|
+
* @throws {UnsafeRecordingIdError} for any id outside the safe domain.
|
|
82
|
+
*/
|
|
83
|
+
export declare function recordingFileName(runId: string): string;
|
|
84
|
+
/** Options for {@link fileRecordingSink}. */
|
|
85
|
+
export interface FileRecordingSinkOptions {
|
|
86
|
+
/** The archive directory. Created if missing, parents included. */
|
|
87
|
+
readonly directory: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A directory-backed recording sink — one JSON file per run, written atomically.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* const recorder = recordRun(agent);
|
|
95
|
+
* await agent.run({ message: 'hi' });
|
|
96
|
+
*
|
|
97
|
+
* await persistRecording(recorder, {
|
|
98
|
+
* sink: fileRecordingSink({ directory: './run-archive' }),
|
|
99
|
+
* run: { complete: true },
|
|
100
|
+
* });
|
|
101
|
+
* // → ./run-archive/run-1787093273110-1.json
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export declare function fileRecordingSink(options: FileRecordingSinkOptions): RecordingSink;
|