agentfootprint 9.47.0 → 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.
Files changed (103) hide show
  1. package/CLAUDE.md +11 -3
  2. package/dist/core/Agent.js +14 -1
  3. package/dist/core/Agent.js.map +1 -1
  4. package/dist/core/agent/AgentBuilder.js +147 -3
  5. package/dist/core/agent/AgentBuilder.js.map +1 -1
  6. package/dist/core/agent/runManifest.js +7 -0
  7. package/dist/core/agent/runManifest.js.map +1 -1
  8. package/dist/doors/recipes.js +55 -0
  9. package/dist/doors/recipes.js.map +1 -0
  10. package/dist/esm/core/Agent.d.ts +9 -1
  11. package/dist/esm/core/Agent.js +14 -1
  12. package/dist/esm/core/Agent.js.map +1 -1
  13. package/dist/esm/core/agent/AgentBuilder.d.ts +68 -0
  14. package/dist/esm/core/agent/AgentBuilder.js +147 -3
  15. package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
  16. package/dist/esm/core/agent/runManifest.d.ts +18 -0
  17. package/dist/esm/core/agent/runManifest.js +7 -0
  18. package/dist/esm/core/agent/runManifest.js.map +1 -1
  19. package/dist/esm/doors/recipes.d.ts +38 -0
  20. package/dist/esm/doors/recipes.js +39 -0
  21. package/dist/esm/doors/recipes.js.map +1 -0
  22. package/dist/esm/events/payloads.d.ts +29 -0
  23. package/dist/esm/observe.d.ts +2 -0
  24. package/dist/esm/observe.js +12 -0
  25. package/dist/esm/observe.js.map +1 -1
  26. package/dist/esm/recipes/apply.d.ts +67 -0
  27. package/dist/esm/recipes/apply.js +117 -0
  28. package/dist/esm/recipes/apply.js.map +1 -0
  29. package/dist/esm/recipes/defineAgentRecipe.d.ts +68 -0
  30. package/dist/esm/recipes/defineAgentRecipe.js +112 -0
  31. package/dist/esm/recipes/defineAgentRecipe.js.map +1 -0
  32. package/dist/esm/recipes/identifier.d.ts +40 -0
  33. package/dist/esm/recipes/identifier.js +95 -0
  34. package/dist/esm/recipes/identifier.js.map +1 -0
  35. package/dist/esm/recipes/index.d.ts +19 -0
  36. package/dist/esm/recipes/index.js +19 -0
  37. package/dist/esm/recipes/index.js.map +1 -0
  38. package/dist/esm/recipes/provenance.d.ts +57 -0
  39. package/dist/esm/recipes/provenance.js +53 -0
  40. package/dist/esm/recipes/provenance.js.map +1 -0
  41. package/dist/esm/recipes/types.d.ts +134 -0
  42. package/dist/esm/recipes/types.js +63 -0
  43. package/dist/esm/recipes/types.js.map +1 -0
  44. package/dist/esm/recipes/version.d.ts +38 -0
  45. package/dist/esm/recipes/version.js +84 -0
  46. package/dist/esm/recipes/version.js.map +1 -0
  47. package/dist/esm/recorders/observability/fileRecordingSink.d.ts +104 -0
  48. package/dist/esm/recorders/observability/fileRecordingSink.js +195 -0
  49. package/dist/esm/recorders/observability/fileRecordingSink.js.map +1 -0
  50. package/dist/esm/recorders/observability/recordingEnvelope.d.ts +292 -0
  51. package/dist/esm/recorders/observability/recordingEnvelope.js +375 -0
  52. package/dist/esm/recorders/observability/recordingEnvelope.js.map +1 -0
  53. package/dist/observe.js +23 -1
  54. package/dist/observe.js.map +1 -1
  55. package/dist/recipes/apply.js +125 -0
  56. package/dist/recipes/apply.js.map +1 -0
  57. package/dist/recipes/defineAgentRecipe.js +118 -0
  58. package/dist/recipes/defineAgentRecipe.js.map +1 -0
  59. package/dist/recipes/identifier.js +100 -0
  60. package/dist/recipes/identifier.js.map +1 -0
  61. package/dist/recipes/index.js +24 -0
  62. package/dist/recipes/index.js.map +1 -0
  63. package/dist/recipes/provenance.js +57 -0
  64. package/dist/recipes/provenance.js.map +1 -0
  65. package/dist/recipes/types.js +64 -0
  66. package/dist/recipes/types.js.map +1 -0
  67. package/dist/recipes/version.js +89 -0
  68. package/dist/recipes/version.js.map +1 -0
  69. package/dist/recorders/observability/fileRecordingSink.js +201 -0
  70. package/dist/recorders/observability/fileRecordingSink.js.map +1 -0
  71. package/dist/recorders/observability/recordingEnvelope.js +382 -0
  72. package/dist/recorders/observability/recordingEnvelope.js.map +1 -0
  73. package/dist/types/core/Agent.d.ts +9 -1
  74. package/dist/types/core/Agent.d.ts.map +1 -1
  75. package/dist/types/core/agent/AgentBuilder.d.ts +68 -0
  76. package/dist/types/core/agent/AgentBuilder.d.ts.map +1 -1
  77. package/dist/types/core/agent/runManifest.d.ts +18 -0
  78. package/dist/types/core/agent/runManifest.d.ts.map +1 -1
  79. package/dist/types/doors/recipes.d.ts +39 -0
  80. package/dist/types/doors/recipes.d.ts.map +1 -0
  81. package/dist/types/events/payloads.d.ts +29 -0
  82. package/dist/types/events/payloads.d.ts.map +1 -1
  83. package/dist/types/observe.d.ts +2 -0
  84. package/dist/types/observe.d.ts.map +1 -1
  85. package/dist/types/recipes/apply.d.ts +68 -0
  86. package/dist/types/recipes/apply.d.ts.map +1 -0
  87. package/dist/types/recipes/defineAgentRecipe.d.ts +69 -0
  88. package/dist/types/recipes/defineAgentRecipe.d.ts.map +1 -0
  89. package/dist/types/recipes/identifier.d.ts +41 -0
  90. package/dist/types/recipes/identifier.d.ts.map +1 -0
  91. package/dist/types/recipes/index.d.ts +20 -0
  92. package/dist/types/recipes/index.d.ts.map +1 -0
  93. package/dist/types/recipes/provenance.d.ts +58 -0
  94. package/dist/types/recipes/provenance.d.ts.map +1 -0
  95. package/dist/types/recipes/types.d.ts +135 -0
  96. package/dist/types/recipes/types.d.ts.map +1 -0
  97. package/dist/types/recipes/version.d.ts +39 -0
  98. package/dist/types/recipes/version.d.ts.map +1 -0
  99. package/dist/types/recorders/observability/fileRecordingSink.d.ts +105 -0
  100. package/dist/types/recorders/observability/fileRecordingSink.d.ts.map +1 -0
  101. package/dist/types/recorders/observability/recordingEnvelope.d.ts +293 -0
  102. package/dist/types/recorders/observability/recordingEnvelope.d.ts.map +1 -0
  103. package/package.json +14 -1
@@ -0,0 +1,68 @@
1
+ /**
2
+ * defineAgentRecipe — declare a named, versioned composition.
3
+ *
4
+ * Pattern: a validating factory that freezes. No class, no registry, no
5
+ * instance state — the "recipes over primitives" instruction taken
6
+ * literally.
7
+ * Role: recipes/ layer, pure. The validation it runs is the SAME function
8
+ * `AgentBuilder.recipe()` runs, so a hand-written literal cannot get
9
+ * past the checks the factory makes; the factory only moves the
10
+ * refusal to the declaration, which is where the fix is.
11
+ * Emits: N/A.
12
+ *
13
+ * ## Why it freezes
14
+ *
15
+ * A recipe is handed to `.recipe()` on one agent and, typically, to `.recipe()`
16
+ * on several more. A mutable one is a shared object that a single consumer can
17
+ * edit for everybody — and the edit would be invisible on the record, because
18
+ * the manifest reports the id and the version, both of which would still say
19
+ * what they always said. `Object.freeze` is shallow, which is exactly the
20
+ * depth that matters here: the four fields are three strings and a function.
21
+ */
22
+ import type { AgentRecipe } from './types.js';
23
+ /**
24
+ * A recipe declaration that cannot be honoured. Thrown by
25
+ * {@link defineAgentRecipe} and by `AgentBuilder.recipe()` — the same class
26
+ * from both doors, because it is the same mistake wherever it is caught.
27
+ */
28
+ export declare class InvalidAgentRecipeError extends Error {
29
+ readonly code: "ERR_INVALID_AGENT_RECIPE";
30
+ /** Which field was refused (`'id'`, `'version'`, `'configure'`, `'shape'`). */
31
+ readonly field: string;
32
+ constructor(field: string, message: string);
33
+ }
34
+ /**
35
+ * Validate a recipe declaration, or refuse it by name.
36
+ *
37
+ * Total over `unknown`: this is the one gate, and it is called from the
38
+ * factory AND from `.recipe()`, so no recipe reaches an agent unvalidated.
39
+ *
40
+ * @param value - the candidate declaration.
41
+ * @param callSite - the API the author called, named in every refusal.
42
+ */
43
+ export declare function assertAgentRecipe(value: unknown, callSite: string): asserts value is AgentRecipe;
44
+ /**
45
+ * Declare a recipe: a name, a version, and the builder calls it stands for.
46
+ *
47
+ * Validates every field and returns a frozen object. Refusals name the field
48
+ * and the fix — `defineAgentRecipe` is where a bad id or version costs one
49
+ * line, and `.recipe()` is where the same mistake costs a stack trace through
50
+ * somebody else's app.
51
+ *
52
+ * @example the composition an app imports and applies
53
+ * ```ts
54
+ * import { defineAgentRecipe } from 'agentfootprint/recipes';
55
+ *
56
+ * export const supportDesk = defineAgentRecipe({
57
+ * id: 'support-desk',
58
+ * version: '1.2.0',
59
+ * description: 'Order lookup + refund policy, the way support runs it.',
60
+ * configure: (agent) => {
61
+ * agent.system('You answer support questions.').tool(lookupOrder);
62
+ * },
63
+ * });
64
+ *
65
+ * const agent = Agent.create({ provider, model }).recipe(supportDesk).build();
66
+ * ```
67
+ */
68
+ export declare function defineAgentRecipe(recipe: AgentRecipe): AgentRecipe;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * defineAgentRecipe — declare a named, versioned composition.
3
+ *
4
+ * Pattern: a validating factory that freezes. No class, no registry, no
5
+ * instance state — the "recipes over primitives" instruction taken
6
+ * literally.
7
+ * Role: recipes/ layer, pure. The validation it runs is the SAME function
8
+ * `AgentBuilder.recipe()` runs, so a hand-written literal cannot get
9
+ * past the checks the factory makes; the factory only moves the
10
+ * refusal to the declaration, which is where the fix is.
11
+ * Emits: N/A.
12
+ *
13
+ * ## Why it freezes
14
+ *
15
+ * A recipe is handed to `.recipe()` on one agent and, typically, to `.recipe()`
16
+ * on several more. A mutable one is a shared object that a single consumer can
17
+ * edit for everybody — and the edit would be invisible on the record, because
18
+ * the manifest reports the id and the version, both of which would still say
19
+ * what they always said. `Object.freeze` is shallow, which is exactly the
20
+ * depth that matters here: the four fields are three strings and a function.
21
+ */
22
+ import { isPlainRecipeId, recipeIdRefusal } from './identifier.js';
23
+ import { isSemverVersion, versionRefusal } from './version.js';
24
+ /** The fields a recipe declares. Anything else is a typo — see the refusal. */
25
+ const RECIPE_KEYS = ['id', 'version', 'description', 'configure'];
26
+ /**
27
+ * A recipe declaration that cannot be honoured. Thrown by
28
+ * {@link defineAgentRecipe} and by `AgentBuilder.recipe()` — the same class
29
+ * from both doors, because it is the same mistake wherever it is caught.
30
+ */
31
+ export class InvalidAgentRecipeError extends Error {
32
+ code = 'ERR_INVALID_AGENT_RECIPE';
33
+ /** Which field was refused (`'id'`, `'version'`, `'configure'`, `'shape'`). */
34
+ field;
35
+ constructor(field, message) {
36
+ super(message);
37
+ this.name = 'InvalidAgentRecipeError';
38
+ this.field = field;
39
+ }
40
+ }
41
+ /**
42
+ * Validate a recipe declaration, or refuse it by name.
43
+ *
44
+ * Total over `unknown`: this is the one gate, and it is called from the
45
+ * factory AND from `.recipe()`, so no recipe reaches an agent unvalidated.
46
+ *
47
+ * @param value - the candidate declaration.
48
+ * @param callSite - the API the author called, named in every refusal.
49
+ */
50
+ export function assertAgentRecipe(value, callSite) {
51
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
52
+ throw new InvalidAgentRecipeError('shape', `${callSite}: a recipe is an object { id, version, description?, configure }, not ` +
53
+ `${value === null ? 'null' : Array.isArray(value) ? 'an array' : typeof value}.`);
54
+ }
55
+ const record = value;
56
+ // Unknown keys first: `name:` instead of `id:` fails every later check with a
57
+ // message about the field that is MISSING, which sends the reader looking for
58
+ // a field they can plainly see they wrote.
59
+ const unknown = Object.keys(record).filter((key) => !RECIPE_KEYS.includes(key));
60
+ if (unknown.length > 0) {
61
+ throw new InvalidAgentRecipeError('shape', `${callSite}: unknown field${unknown.length > 1 ? 's' : ''} ${unknown
62
+ .map((k) => `'${k}'`)
63
+ .join(', ')}. A recipe declares exactly ${RECIPE_KEYS.map((k) => `\`${k}\``).join(', ')} ` +
64
+ `— everything else about the agent is expressed by the builder calls \`configure\` ` +
65
+ `makes, which is the whole point of composing over the builder instead of inventing a ` +
66
+ `second configuration format.`);
67
+ }
68
+ if (!isPlainRecipeId(record.id)) {
69
+ throw new InvalidAgentRecipeError('id', recipeIdRefusal(callSite, record.id));
70
+ }
71
+ if (!isSemverVersion(record.version)) {
72
+ throw new InvalidAgentRecipeError('version', versionRefusal(callSite, record.version));
73
+ }
74
+ if (record.description !== undefined && typeof record.description !== 'string') {
75
+ throw new InvalidAgentRecipeError('description', `${callSite}: description must be a string (one sentence saying what this composition is ` +
76
+ `for), or omitted. Got ${typeof record.description}.`);
77
+ }
78
+ if (typeof record.configure !== 'function') {
79
+ throw new InvalidAgentRecipeError('configure', `${callSite}: configure must be a function (builder) => void — the builder calls this ` +
80
+ `composition stands for. Got ${record.configure === undefined ? 'nothing' : typeof record.configure}. A recipe with no \`configure\` configures nothing: it would apply cleanly, change ` +
81
+ `no behaviour, and still put a row on the run manifest claiming it shaped the agent.`);
82
+ }
83
+ }
84
+ /**
85
+ * Declare a recipe: a name, a version, and the builder calls it stands for.
86
+ *
87
+ * Validates every field and returns a frozen object. Refusals name the field
88
+ * and the fix — `defineAgentRecipe` is where a bad id or version costs one
89
+ * line, and `.recipe()` is where the same mistake costs a stack trace through
90
+ * somebody else's app.
91
+ *
92
+ * @example the composition an app imports and applies
93
+ * ```ts
94
+ * import { defineAgentRecipe } from 'agentfootprint/recipes';
95
+ *
96
+ * export const supportDesk = defineAgentRecipe({
97
+ * id: 'support-desk',
98
+ * version: '1.2.0',
99
+ * description: 'Order lookup + refund policy, the way support runs it.',
100
+ * configure: (agent) => {
101
+ * agent.system('You answer support questions.').tool(lookupOrder);
102
+ * },
103
+ * });
104
+ *
105
+ * const agent = Agent.create({ provider, model }).recipe(supportDesk).build();
106
+ * ```
107
+ */
108
+ export function defineAgentRecipe(recipe) {
109
+ assertAgentRecipe(recipe, 'defineAgentRecipe');
110
+ return Object.freeze({ ...recipe });
111
+ }
112
+ //# sourceMappingURL=defineAgentRecipe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defineAgentRecipe.js","sourceRoot":"","sources":["../../../src/recipes/defineAgentRecipe.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * identifier — the plain-name rule for a recipe id.
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
+ * ## Two rules, and the second one is the interesting one
10
+ *
11
+ * 1. **A plain name.** Lower-case words joined by single hyphens:
12
+ * `support-desk`, `triage`, `refund-policy`. The id is what a person reads
13
+ * on a run manifest, in a conflict refusal and in a bug report, so it is
14
+ * spelled the way the rest of this library's public names are — for the
15
+ * common reader, not for the implementation.
16
+ *
17
+ * 2. **No version suffix.** `support-desk-2` and `support-desk-v2` are refused.
18
+ * A recipe already HAS a version field; an id that also encodes one produces
19
+ * two names for one composition, and then nothing groups: runs of `-2` and
20
+ * runs of the original look like two unrelated agents on the record, while
21
+ * the field that exists to tell them apart says `1.0.0` on both.
22
+ *
23
+ * ## The honest limit of rule 2
24
+ *
25
+ * It matches the version-suffix SHAPES — a final hyphen-separated segment that
26
+ * is nothing but digits, optionally preceded by `v`. It does NOT catch an id
27
+ * that merely ends in a digit, because `oauth2`, `s3-archive` and `sha256` are
28
+ * real words and refusing them would be worse than missing `triage2`. Stated
29
+ * rather than implied: this check narrows a mistake, it does not eliminate it.
30
+ */
31
+ /** Whether `value` is a plain recipe id. Total: any input, no throw. */
32
+ export declare function isPlainRecipeId(value: unknown): value is string;
33
+ /**
34
+ * The sentence a bad id gets. Names the value and the specific rule it broke —
35
+ * the version-suffix case in particular, because the fix there is not
36
+ * "spell it differently" but "use the field that already exists".
37
+ *
38
+ * @param callSite - the API the author called, e.g. `defineAgentRecipe`.
39
+ */
40
+ export declare function recipeIdRefusal(callSite: string, value: unknown): string;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * identifier — the plain-name rule for a recipe id.
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
+ * ## Two rules, and the second one is the interesting one
10
+ *
11
+ * 1. **A plain name.** Lower-case words joined by single hyphens:
12
+ * `support-desk`, `triage`, `refund-policy`. The id is what a person reads
13
+ * on a run manifest, in a conflict refusal and in a bug report, so it is
14
+ * spelled the way the rest of this library's public names are — for the
15
+ * common reader, not for the implementation.
16
+ *
17
+ * 2. **No version suffix.** `support-desk-2` and `support-desk-v2` are refused.
18
+ * A recipe already HAS a version field; an id that also encodes one produces
19
+ * two names for one composition, and then nothing groups: runs of `-2` and
20
+ * runs of the original look like two unrelated agents on the record, while
21
+ * the field that exists to tell them apart says `1.0.0` on both.
22
+ *
23
+ * ## The honest limit of rule 2
24
+ *
25
+ * It matches the version-suffix SHAPES — a final hyphen-separated segment that
26
+ * is nothing but digits, optionally preceded by `v`. It does NOT catch an id
27
+ * that merely ends in a digit, because `oauth2`, `s3-archive` and `sha256` are
28
+ * real words and refusing them would be worse than missing `triage2`. Stated
29
+ * rather than implied: this check narrows a mistake, it does not eliminate it.
30
+ */
31
+ /** Lower-kebab: starts with a letter, single hyphens, no trailing hyphen. */
32
+ const PLAIN_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
33
+ /** A final segment that is a version: `2`, `07`, `v2`. */
34
+ const VERSION_SUFFIX = /(?:^|-)v?\d+$/;
35
+ /** Long enough for a sentence-like name, short enough to read in a refusal. */
36
+ const MAX_LENGTH = 64;
37
+ /** Whether `value` is a plain recipe id. Total: any input, no throw. */
38
+ export function isPlainRecipeId(value) {
39
+ return (typeof value === 'string' &&
40
+ value.length <= MAX_LENGTH &&
41
+ PLAIN_NAME.test(value) &&
42
+ !VERSION_SUFFIX.test(value));
43
+ }
44
+ /**
45
+ * The sentence a bad id gets. Names the value and the specific rule it broke —
46
+ * the version-suffix case in particular, because the fix there is not
47
+ * "spell it differently" but "use the field that already exists".
48
+ *
49
+ * @param callSite - the API the author called, e.g. `defineAgentRecipe`.
50
+ */
51
+ export function recipeIdRefusal(callSite, value) {
52
+ const shown = typeof value === 'string' ? `'${value}'` : `${typeof value}`;
53
+ if (typeof value === 'string' && PLAIN_NAME.test(value) && VERSION_SUFFIX.test(value)) {
54
+ const base = value.replace(VERSION_SUFFIX, '');
55
+ return (`${callSite}: id ${shown} ends in a version suffix. A recipe carries its version in the ` +
56
+ `\`version\` field, so an id that encodes one too gives a single composition two names — ` +
57
+ `and then nothing groups: runs of ${shown} and runs of '${base}' read as two unrelated ` +
58
+ `agents on the manifest, while the field that exists to tell them apart says the same ` +
59
+ `thing on both.\n\n` +
60
+ `Use id: '${base || 'a-plain-name'}' and bump \`version\` instead.`);
61
+ }
62
+ return (`${callSite}: id ${shown} is not a plain name. A recipe id is lower-case words joined by ` +
63
+ `single hyphens, starting with a letter, at most ${MAX_LENGTH} characters — ` +
64
+ `'support-desk', 'triage', 'refund-policy'.\n\n` +
65
+ `${diagnose(value)}\n\n` +
66
+ `It is spelled for the person who reads it on a run manifest, in a conflict refusal and in ` +
67
+ `a bug report — not for the code.`);
68
+ }
69
+ /** The specific rule broken, when the shape names one. */
70
+ function diagnose(value) {
71
+ if (typeof value !== 'string') {
72
+ return `An id is a string; this was ${value === null ? 'null' : typeof value}.`;
73
+ }
74
+ if (value === '')
75
+ return 'This one is empty.';
76
+ if (value.length > MAX_LENGTH)
77
+ return `This one is ${value.length} characters.`;
78
+ if (value !== value.trim())
79
+ return 'This one has leading or trailing whitespace.';
80
+ if (/[A-Z]/.test(value)) {
81
+ return `Lower-case only — write '${value
82
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
83
+ .toLowerCase()}'.`;
84
+ }
85
+ if (/\s/.test(value))
86
+ return `Join the words with hyphens — '${value.trim().replace(/\s+/g, '-')}'.`;
87
+ if (/^[^a-z]/.test(value))
88
+ return 'It has to start with a letter.';
89
+ if (/-$/.test(value))
90
+ return 'It cannot end with a hyphen.';
91
+ if (/--/.test(value))
92
+ return 'Single hyphens only.';
93
+ return 'Allowed characters are a–z, 0–9 and single hyphens between them.';
94
+ }
95
+ //# sourceMappingURL=identifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identifier.js","sourceRoot":"","sources":["../../../src/recipes/identifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,6EAA6E;AAC7E,MAAM,UAAU,GAAG,iCAAiC,CAAC;AAErD,0DAA0D;AAC1D,MAAM,cAAc,GAAG,eAAe,CAAC;AAEvC,+EAA+E;AAC/E,MAAM,UAAU,GAAG,EAAE,CAAC;AAEtB,wEAAwE;AACxE,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,CAAC,MAAM,IAAI,UAAU;QAC1B,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC;QACtB,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAC5B,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,KAAc;IAC9D,MAAM,KAAK,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,OAAO,KAAK,EAAE,CAAC;IAC3E,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACtF,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;QAC/C,OAAO,CACL,GAAG,QAAQ,QAAQ,KAAK,iEAAiE;YACzF,0FAA0F;YAC1F,oCAAoC,KAAK,iBAAiB,IAAI,0BAA0B;YACxF,uFAAuF;YACvF,oBAAoB;YACpB,YAAY,IAAI,IAAI,cAAc,iCAAiC,CACpE,CAAC;IACJ,CAAC;IACD,OAAO,CACL,GAAG,QAAQ,QAAQ,KAAK,kEAAkE;QAC1F,mDAAmD,UAAU,gBAAgB;QAC7E,gDAAgD;QAChD,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM;QACxB,4FAA4F;QAC5F,kCAAkC,CACnC,CAAC;AACJ,CAAC;AAED,0DAA0D;AAC1D,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,+BAA+B,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,KAAK,GAAG,CAAC;IAClF,CAAC;IACD,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,oBAAoB,CAAC;IAC9C,IAAI,KAAK,CAAC,MAAM,GAAG,UAAU;QAAE,OAAO,eAAe,KAAK,CAAC,MAAM,cAAc,CAAC;IAChF,IAAI,KAAK,KAAK,KAAK,CAAC,IAAI,EAAE;QAAE,OAAO,8CAA8C,CAAC;IAClF,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,4BAA4B,KAAK;aACrC,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC;aACtC,WAAW,EAAE,IAAI,CAAC;IACvB,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;QAClB,OAAO,kCAAkC,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC;IACjF,IAAI,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,gCAAgC,CAAC;IACnE,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,8BAA8B,CAAC;IAC5D,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,sBAAsB,CAAC;IACpD,OAAO,kEAAkE,CAAC;AAC5E,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * recipes — the declared unit of agent configuration.
3
+ *
4
+ * The door: what `agentfootprint/recipes` publishes. Read `types.ts` for the
5
+ * argument (including the no-lifecycle limit) and README.md for the worked
6
+ * example.
7
+ *
8
+ * ## What is NOT here, and why
9
+ *
10
+ * The validators (`isPlainRecipeId`, `isSemverVersion`), the provenance
11
+ * formatter and every refusal sentence are internal. `AgentBuilder` imports
12
+ * them by module path, and this door stays the AUTHORING vocabulary: declare a
13
+ * recipe, catch the refusal, name the types. Publishing the plumbing would be
14
+ * surface nobody imports and everybody has to keep documented — and a recipe's
15
+ * whole claim is that it is a composition over primitives, not a second
16
+ * framework with an API of its own.
17
+ */
18
+ export { defineAgentRecipe, InvalidAgentRecipeError } from './defineAgentRecipe.js';
19
+ export type { AgentRecipe, AppliedRecipe, RecipeConflictPolicy, RecipeOptions } from './types.js';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * recipes — the declared unit of agent configuration.
3
+ *
4
+ * The door: what `agentfootprint/recipes` publishes. Read `types.ts` for the
5
+ * argument (including the no-lifecycle limit) and README.md for the worked
6
+ * example.
7
+ *
8
+ * ## What is NOT here, and why
9
+ *
10
+ * The validators (`isPlainRecipeId`, `isSemverVersion`), the provenance
11
+ * formatter and every refusal sentence are internal. `AgentBuilder` imports
12
+ * them by module path, and this door stays the AUTHORING vocabulary: declare a
13
+ * recipe, catch the refusal, name the types. Publishing the plumbing would be
14
+ * surface nobody imports and everybody has to keep documented — and a recipe's
15
+ * whole claim is that it is a composition over primitives, not a second
16
+ * framework with an API of its own.
17
+ */
18
+ export { defineAgentRecipe, InvalidAgentRecipeError } from './defineAgentRecipe.js';
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/recipes/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,57 @@
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
+ import type { AppliedRecipe } from './types.js';
36
+ /**
37
+ * Where a registered tool name or injection id came from.
38
+ *
39
+ * `stack` is the recipe application chain, OUTERMOST first: a recipe may apply
40
+ * another recipe, and the innermost one is the code that literally called the
41
+ * builder method.
42
+ */
43
+ export type RecipeSource = {
44
+ readonly kind: 'local';
45
+ } | {
46
+ readonly kind: 'recipe';
47
+ readonly stack: readonly AppliedRecipe[];
48
+ };
49
+ /** The one `local` value. A frozen singleton — it carries no data. */
50
+ export declare const LOCAL_SOURCE: RecipeSource;
51
+ /**
52
+ * Describe a source in a sentence fragment that reads inside a refusal.
53
+ *
54
+ * `undefined` is the unattributed case — see the header. It is a separate
55
+ * answer, never folded into `local`.
56
+ */
57
+ export declare function describeRecipeSource(source: RecipeSource | undefined): string;
@@ -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
+ }