agentfootprint 8.3.0 → 8.5.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/AGENTS.md +11 -8
- package/CLAUDE.md +4 -3
- package/ai-instructions/setup.sh +0 -0
- package/bin/agentfootprint-lint-tools.mjs +0 -0
- package/dist/core/Agent.js +107 -2
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/agent/AgentBuilder.js +47 -5
- package/dist/core/agent/AgentBuilder.js.map +1 -1
- package/dist/core/agent/buildAgentChart.js +5 -0
- package/dist/core/agent/buildAgentChart.js.map +1 -1
- package/dist/core/agent/buildDynamicAgentChart.js +6 -0
- package/dist/core/agent/buildDynamicAgentChart.js.map +1 -1
- package/dist/core/agent/stages/toolCalls.js +53 -7
- package/dist/core/agent/stages/toolCalls.js.map +1 -1
- package/dist/core/slots/buildToolsSlot.js +9 -1
- package/dist/core/slots/buildToolsSlot.js.map +1 -1
- package/dist/esm/core/Agent.d.ts +62 -2
- package/dist/esm/core/Agent.js +107 -2
- package/dist/esm/core/Agent.js.map +1 -1
- package/dist/esm/core/agent/AgentBuilder.d.ts +30 -1
- package/dist/esm/core/agent/AgentBuilder.js +47 -5
- package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
- package/dist/esm/core/agent/buildAgentChart.js +5 -0
- package/dist/esm/core/agent/buildAgentChart.js.map +1 -1
- package/dist/esm/core/agent/buildDynamicAgentChart.js +6 -0
- package/dist/esm/core/agent/buildDynamicAgentChart.js.map +1 -1
- package/dist/esm/core/agent/stages/toolCalls.d.ts +17 -0
- package/dist/esm/core/agent/stages/toolCalls.js +53 -7
- package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
- package/dist/esm/core/slots/buildToolsSlot.d.ts +13 -0
- package/dist/esm/core/slots/buildToolsSlot.js +9 -1
- package/dist/esm/core/slots/buildToolsSlot.js.map +1 -1
- package/dist/esm/events/payloads.d.ts +17 -0
- package/dist/esm/lib/injection-engine/buildInjectionEngineSubflow.d.ts +12 -0
- package/dist/esm/lib/injection-engine/buildInjectionEngineSubflow.js +28 -4
- package/dist/esm/lib/injection-engine/buildInjectionEngineSubflow.js.map +1 -1
- package/dist/esm/lib/injection-engine/index.d.ts +1 -1
- package/dist/esm/lib/injection-engine/index.js.map +1 -1
- package/dist/esm/lib/injection-engine/skillBodyDelivery.d.ts +65 -0
- package/dist/esm/lib/injection-engine/skillBodyDelivery.js +110 -0
- package/dist/esm/lib/injection-engine/skillBodyDelivery.js.map +1 -0
- package/dist/esm/lib/injection-engine/skillGraph.d.ts +138 -39
- package/dist/esm/lib/injection-engine/skillGraph.js +178 -33
- package/dist/esm/lib/injection-engine/skillGraph.js.map +1 -1
- package/dist/esm/lib/injection-engine/skillTools.d.ts +22 -1
- package/dist/esm/lib/injection-engine/skillTools.js +39 -7
- package/dist/esm/lib/injection-engine/skillTools.js.map +1 -1
- package/dist/esm/recorders/observability/RouteRecorder.d.ts +11 -2
- package/dist/esm/recorders/observability/RouteRecorder.js +58 -6
- package/dist/esm/recorders/observability/RouteRecorder.js.map +1 -1
- package/dist/lib/injection-engine/buildInjectionEngineSubflow.js +28 -4
- package/dist/lib/injection-engine/buildInjectionEngineSubflow.js.map +1 -1
- package/dist/lib/injection-engine/index.js.map +1 -1
- package/dist/lib/injection-engine/skillBodyDelivery.js +116 -0
- package/dist/lib/injection-engine/skillBodyDelivery.js.map +1 -0
- package/dist/lib/injection-engine/skillGraph.js +178 -33
- package/dist/lib/injection-engine/skillGraph.js.map +1 -1
- package/dist/lib/injection-engine/skillTools.js +39 -7
- package/dist/lib/injection-engine/skillTools.js.map +1 -1
- package/dist/recorders/observability/RouteRecorder.js +58 -6
- package/dist/recorders/observability/RouteRecorder.js.map +1 -1
- package/dist/types/core/Agent.d.ts +62 -2
- package/dist/types/core/Agent.d.ts.map +1 -1
- package/dist/types/core/agent/AgentBuilder.d.ts +30 -1
- package/dist/types/core/agent/AgentBuilder.d.ts.map +1 -1
- package/dist/types/core/agent/buildAgentChart.d.ts.map +1 -1
- package/dist/types/core/agent/buildDynamicAgentChart.d.ts.map +1 -1
- package/dist/types/core/agent/stages/toolCalls.d.ts +17 -0
- package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
- package/dist/types/core/slots/buildToolsSlot.d.ts +13 -0
- package/dist/types/core/slots/buildToolsSlot.d.ts.map +1 -1
- package/dist/types/events/payloads.d.ts +17 -0
- package/dist/types/events/payloads.d.ts.map +1 -1
- package/dist/types/lib/injection-engine/buildInjectionEngineSubflow.d.ts +12 -0
- package/dist/types/lib/injection-engine/buildInjectionEngineSubflow.d.ts.map +1 -1
- package/dist/types/lib/injection-engine/index.d.ts +1 -1
- package/dist/types/lib/injection-engine/index.d.ts.map +1 -1
- package/dist/types/lib/injection-engine/skillBodyDelivery.d.ts +66 -0
- package/dist/types/lib/injection-engine/skillBodyDelivery.d.ts.map +1 -0
- package/dist/types/lib/injection-engine/skillGraph.d.ts +138 -39
- package/dist/types/lib/injection-engine/skillGraph.d.ts.map +1 -1
- package/dist/types/lib/injection-engine/skillTools.d.ts +22 -1
- package/dist/types/lib/injection-engine/skillTools.d.ts.map +1 -1
- package/dist/types/recorders/observability/RouteRecorder.d.ts +11 -2
- package/dist/types/recorders/observability/RouteRecorder.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a skill's BODY actually lands — and the one combination where the honest
|
|
3
|
+
* answer is "nowhere" (8.5.0).
|
|
4
|
+
*
|
|
5
|
+
* `surfaceMode` names a delivery CHANNEL, and `'tool-only'` names the channel that
|
|
6
|
+
* only exists when the model calls `read_skill`: the body is returned as that tool's
|
|
7
|
+
* result, and the system slot suppresses it by design (`buildSystemPromptSlot`,
|
|
8
|
+
* mirrored in `routeActiveInjections`). That is exactly right for a skill `read_skill`
|
|
9
|
+
* activates, which is what the mode was designed against and what every shipped
|
|
10
|
+
* Block-C test covers.
|
|
11
|
+
*
|
|
12
|
+
* It is a hole for a skill the GRAPH activates. A route target, a flat entry and a
|
|
13
|
+
* decision-tree leaf all activate off the cursor, without any `read_skill` call — so
|
|
14
|
+
* the tool result never happens, the system slot suppresses the body anyway, and the
|
|
15
|
+
* body reaches the model through no channel at all. The skill's TOOLS still arrive,
|
|
16
|
+
* which makes it worse than the skill not loading: the model is handed the tools of a
|
|
17
|
+
* procedure it was never told.
|
|
18
|
+
*
|
|
19
|
+
* The rule that closes it is the one 8.4.0 already uses for the `read_skill` gate:
|
|
20
|
+
* `trigger.kind === 'llm-activated'` is precisely "read_skill can really activate
|
|
21
|
+
* this". A skill may claim the read_skill delivery channel exactly when read_skill is
|
|
22
|
+
* what activates it.
|
|
23
|
+
*
|
|
24
|
+
* Refusal rather than a quiet fall back to the system slot, because the author wrote
|
|
25
|
+
* `'tool-only'` to keep the body OUT of the system prompt (token cost, attention
|
|
26
|
+
* placement). Silently putting it back would honour the activation and break the
|
|
27
|
+
* declaration — a different lie, not a fix. `'both'` already means "deliver it either
|
|
28
|
+
* way", so the refusal has a real, one-word answer to name.
|
|
29
|
+
*/
|
|
30
|
+
import type { Injection } from './types.js';
|
|
31
|
+
import { type SurfaceMode } from './factories/defineSkill.js';
|
|
32
|
+
/**
|
|
33
|
+
* The surface mode a skill will be TREATED as at runtime.
|
|
34
|
+
*
|
|
35
|
+
* Today the runtime compares the literal string, so `'auto'` is its own mode and
|
|
36
|
+
* lands in the system slot — pass no provider and that is what you get back, which
|
|
37
|
+
* is the truth about the current engine.
|
|
38
|
+
*
|
|
39
|
+
* Pass a provider (and model) to ask the OTHER question: what would `'auto'` become
|
|
40
|
+
* if the `resolveSurfaceMode` cascade were wired into the runtime? It resolves to
|
|
41
|
+
* `'tool-only'` for every non-Claude provider, so wiring it in without this guard
|
|
42
|
+
* would silently open the delivered-nowhere hole for every OpenAI / Bedrock / Ollama
|
|
43
|
+
* user at once. Routing both questions through ONE function is what keeps that from
|
|
44
|
+
* being a future accident: the refusal below is written against this, so the day the
|
|
45
|
+
* cascade is wired in, the guard already covers it.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolvedSurfaceModeOf(skill: Injection, provider?: string, model?: string): SurfaceMode;
|
|
48
|
+
/**
|
|
49
|
+
* Can this skill's body be delivered through the `read_skill` tool result?
|
|
50
|
+
*
|
|
51
|
+
* Only if `read_skill` is what activates it. `'llm-activated'` is the one trigger
|
|
52
|
+
* kind that reads `activatedInjectionIds`, which is the only thing a `read_skill`
|
|
53
|
+
* call writes — the same clause the gate's open-skill rule turns on (8.4.0).
|
|
54
|
+
*/
|
|
55
|
+
export declare function activatesByRead(skill: Injection): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Refuse every skill that claims the `read_skill` delivery channel without being
|
|
58
|
+
* activated by `read_skill`. Returns the message, or `undefined` when all is well.
|
|
59
|
+
*
|
|
60
|
+
* Runs over the FINAL injection list — the agent is the only place that sees every
|
|
61
|
+
* skill's compiled trigger, whichever call registered it — and names every offender,
|
|
62
|
+
* because `skillsFromDir({ surfaceMode: 'tool-only' })` under a graph refuses a whole
|
|
63
|
+
* directory at once and a list is debuggable where one sample is not.
|
|
64
|
+
*/
|
|
65
|
+
export declare function toolOnlyDeliveryRefusal(injections: readonly Injection[]): string | undefined;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a skill's BODY actually lands — and the one combination where the honest
|
|
3
|
+
* answer is "nowhere" (8.5.0).
|
|
4
|
+
*
|
|
5
|
+
* `surfaceMode` names a delivery CHANNEL, and `'tool-only'` names the channel that
|
|
6
|
+
* only exists when the model calls `read_skill`: the body is returned as that tool's
|
|
7
|
+
* result, and the system slot suppresses it by design (`buildSystemPromptSlot`,
|
|
8
|
+
* mirrored in `routeActiveInjections`). That is exactly right for a skill `read_skill`
|
|
9
|
+
* activates, which is what the mode was designed against and what every shipped
|
|
10
|
+
* Block-C test covers.
|
|
11
|
+
*
|
|
12
|
+
* It is a hole for a skill the GRAPH activates. A route target, a flat entry and a
|
|
13
|
+
* decision-tree leaf all activate off the cursor, without any `read_skill` call — so
|
|
14
|
+
* the tool result never happens, the system slot suppresses the body anyway, and the
|
|
15
|
+
* body reaches the model through no channel at all. The skill's TOOLS still arrive,
|
|
16
|
+
* which makes it worse than the skill not loading: the model is handed the tools of a
|
|
17
|
+
* procedure it was never told.
|
|
18
|
+
*
|
|
19
|
+
* The rule that closes it is the one 8.4.0 already uses for the `read_skill` gate:
|
|
20
|
+
* `trigger.kind === 'llm-activated'` is precisely "read_skill can really activate
|
|
21
|
+
* this". A skill may claim the read_skill delivery channel exactly when read_skill is
|
|
22
|
+
* what activates it.
|
|
23
|
+
*
|
|
24
|
+
* Refusal rather than a quiet fall back to the system slot, because the author wrote
|
|
25
|
+
* `'tool-only'` to keep the body OUT of the system prompt (token cost, attention
|
|
26
|
+
* placement). Silently putting it back would honour the activation and break the
|
|
27
|
+
* declaration — a different lie, not a fix. `'both'` already means "deliver it either
|
|
28
|
+
* way", so the refusal has a real, one-word answer to name.
|
|
29
|
+
*/
|
|
30
|
+
import { resolveSurfaceMode } from './factories/defineSkill.js';
|
|
31
|
+
/** A skill's declared surface mode, before any provider resolution. */
|
|
32
|
+
function declaredSurfaceModeOf(skill) {
|
|
33
|
+
const meta = skill.metadata;
|
|
34
|
+
return meta?.surfaceMode ?? 'auto';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The surface mode a skill will be TREATED as at runtime.
|
|
38
|
+
*
|
|
39
|
+
* Today the runtime compares the literal string, so `'auto'` is its own mode and
|
|
40
|
+
* lands in the system slot — pass no provider and that is what you get back, which
|
|
41
|
+
* is the truth about the current engine.
|
|
42
|
+
*
|
|
43
|
+
* Pass a provider (and model) to ask the OTHER question: what would `'auto'` become
|
|
44
|
+
* if the `resolveSurfaceMode` cascade were wired into the runtime? It resolves to
|
|
45
|
+
* `'tool-only'` for every non-Claude provider, so wiring it in without this guard
|
|
46
|
+
* would silently open the delivered-nowhere hole for every OpenAI / Bedrock / Ollama
|
|
47
|
+
* user at once. Routing both questions through ONE function is what keeps that from
|
|
48
|
+
* being a future accident: the refusal below is written against this, so the day the
|
|
49
|
+
* cascade is wired in, the guard already covers it.
|
|
50
|
+
*/
|
|
51
|
+
export function resolvedSurfaceModeOf(skill, provider, model) {
|
|
52
|
+
const declared = declaredSurfaceModeOf(skill);
|
|
53
|
+
if (declared !== 'auto' || provider === undefined)
|
|
54
|
+
return declared;
|
|
55
|
+
return resolveSurfaceMode(provider, model);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Can this skill's body be delivered through the `read_skill` tool result?
|
|
59
|
+
*
|
|
60
|
+
* Only if `read_skill` is what activates it. `'llm-activated'` is the one trigger
|
|
61
|
+
* kind that reads `activatedInjectionIds`, which is the only thing a `read_skill`
|
|
62
|
+
* call writes — the same clause the gate's open-skill rule turns on (8.4.0).
|
|
63
|
+
*/
|
|
64
|
+
export function activatesByRead(skill) {
|
|
65
|
+
return skill.trigger.kind === 'llm-activated';
|
|
66
|
+
}
|
|
67
|
+
/** How a refused skill is identified back to its author, so a directory-sized
|
|
68
|
+
* refusal reads as a list of names and reasons rather than one mystery. */
|
|
69
|
+
function describeRouting(skill) {
|
|
70
|
+
const routing = skill.metadata
|
|
71
|
+
?.skillGraph;
|
|
72
|
+
if (routing?.via === 'route') {
|
|
73
|
+
return routing.from
|
|
74
|
+
? `a route target (the graph routes "${routing.from}" → "${skill.id}")`
|
|
75
|
+
: 'a route target';
|
|
76
|
+
}
|
|
77
|
+
if (routing?.via === 'entry')
|
|
78
|
+
return 'a graph entry';
|
|
79
|
+
if (routing?.via === 'tree')
|
|
80
|
+
return 'a decision-tree leaf';
|
|
81
|
+
return `activated by rule (trigger '${skill.trigger.kind}')`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Refuse every skill that claims the `read_skill` delivery channel without being
|
|
85
|
+
* activated by `read_skill`. Returns the message, or `undefined` when all is well.
|
|
86
|
+
*
|
|
87
|
+
* Runs over the FINAL injection list — the agent is the only place that sees every
|
|
88
|
+
* skill's compiled trigger, whichever call registered it — and names every offender,
|
|
89
|
+
* because `skillsFromDir({ surfaceMode: 'tool-only' })` under a graph refuses a whole
|
|
90
|
+
* directory at once and a list is debuggable where one sample is not.
|
|
91
|
+
*/
|
|
92
|
+
export function toolOnlyDeliveryRefusal(injections) {
|
|
93
|
+
const offenders = injections.filter((i) => i.flavor === 'skill' &&
|
|
94
|
+
resolvedSurfaceModeOf(i) === 'tool-only' &&
|
|
95
|
+
!activatesByRead(i) &&
|
|
96
|
+
(i.inject.systemPrompt ?? '').length > 0);
|
|
97
|
+
if (offenders.length === 0)
|
|
98
|
+
return undefined;
|
|
99
|
+
const list = offenders.map((s) => ` • "${s.id}" — ${describeRouting(s)}`).join('\n');
|
|
100
|
+
const subject = offenders.length === 1 ? 'This skill sets' : 'These skills set';
|
|
101
|
+
const its = offenders.length === 1 ? 'its' : 'their';
|
|
102
|
+
return (`Agent: ${subject} surfaceMode: 'tool-only', which delivers the body as the ` +
|
|
103
|
+
`read_skill tool result — but nothing here activates by read_skill, so ${its} ` +
|
|
104
|
+
`body would reach the model NOWHERE: the system slot suppresses a tool-only body ` +
|
|
105
|
+
`by design, and no read_skill call happens to carry it.\n${list}\n` +
|
|
106
|
+
`Use 'both' (system prompt AND tool result) or 'system-prompt'. Only a skill ` +
|
|
107
|
+
`read_skill actually activates — trigger 'llm-activated', which is every skill a ` +
|
|
108
|
+
`graph does not route — can be 'tool-only'.`);
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=skillBodyDelivery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skillBodyDelivery.js","sourceRoot":"","sources":["../../../../src/lib/injection-engine/skillBodyDelivery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAGH,OAAO,EAAE,kBAAkB,EAAoB,MAAM,4BAA4B,CAAC;AAElF,uEAAuE;AACvE,SAAS,qBAAqB,CAAC,KAAgB;IAC7C,MAAM,IAAI,GAAG,KAAK,CAAC,QAAqD,CAAC;IACzE,OAAO,IAAI,EAAE,WAAW,IAAI,MAAM,CAAC;AACrC,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,qBAAqB,CACnC,KAAgB,EAChB,QAAiB,EACjB,KAAc;IAEd,MAAM,QAAQ,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAC;IAC9C,IAAI,QAAQ,KAAK,MAAM,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IACnE,OAAO,kBAAkB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;AAC7C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,KAAgB;IAC9C,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,eAAe,CAAC;AAChD,CAAC;AAED;4EAC4E;AAC5E,SAAS,eAAe,CAAC,KAAgB;IACvC,MAAM,OAAO,GAAI,KAAK,CAAC,QAAyE;QAC9F,EAAE,UAAU,CAAC;IACf,IAAI,OAAO,EAAE,GAAG,KAAK,OAAO,EAAE,CAAC;QAC7B,OAAO,OAAO,CAAC,IAAI;YACjB,CAAC,CAAC,qCAAqC,OAAO,CAAC,IAAI,QAAQ,KAAK,CAAC,EAAE,IAAI;YACvE,CAAC,CAAC,gBAAgB,CAAC;IACvB,CAAC;IACD,IAAI,OAAO,EAAE,GAAG,KAAK,OAAO;QAAE,OAAO,eAAe,CAAC;IACrD,IAAI,OAAO,EAAE,GAAG,KAAK,MAAM;QAAE,OAAO,sBAAsB,CAAC;IAC3D,OAAO,+BAA+B,KAAK,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;AAC/D,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,uBAAuB,CAAC,UAAgC;IACtE,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,CACjC,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,MAAM,KAAK,OAAO;QACpB,qBAAqB,CAAC,CAAC,CAAC,KAAK,WAAW;QACxC,CAAC,eAAe,CAAC,CAAC,CAAC;QACnB,CAAC,CAAC,CAAC,MAAM,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAC3C,CAAC;IACF,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAE7C,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,OAAO,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtF,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,kBAAkB,CAAC;IAChF,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC;IACrD,OAAO,CACL,UAAU,OAAO,4DAA4D;QAC7E,yEAAyE,GAAG,GAAG;QAC/E,kFAAkF;QAClF,2DAA2D,IAAI,IAAI;QACnE,8EAA8E;QAC9E,kFAAkF;QAClF,4CAA4C,CAC7C,CAAC;AACJ,CAAC"}
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
*
|
|
13
13
|
* **v2 keystone — `from` IS enforced (a sticky cursor state machine).** A skill
|
|
14
14
|
* graph is a state machine over skills; the engine tracks which node it is in via
|
|
15
|
-
* `InjectionContext.currentSkillId` (the cursor). One pure resolver — `
|
|
16
|
-
* (see `
|
|
15
|
+
* `InjectionContext.currentSkillId` (the cursor). One pure resolver — `resolveCursor`
|
|
16
|
+
* (see `makeResolveCursor`), of which `nextSkill(ctx)` is the `.to` projection — is the
|
|
17
|
+
* single source of truth: each route target B
|
|
17
18
|
* compiles to the trigger `nextSkill(ctx) === B`, which delivers `from`-gating
|
|
18
19
|
* (an edge `A→B` fires only while the cursor is on A — no cross-skill edge bleed),
|
|
19
20
|
* stickiness (the cursor stays on B until an edge leaves B), and a clean handoff
|
|
@@ -24,16 +25,26 @@
|
|
|
24
25
|
* compiled trigger is always a `rule`. `toMermaid()` renders declared === drawn.
|
|
25
26
|
*
|
|
26
27
|
* A decision `tree()` routes per-iteration by stable `ctx` predicates (no cursor)
|
|
27
|
-
* and is unaffected by `from`-gating.
|
|
28
|
+
* and is unaffected by `from`-gating. It has no cursor for `read_skill` to move
|
|
29
|
+
* either, so `reachableSkills()` is EMPTY there and the gate refuses a leaf pick
|
|
30
|
+
* (8.5.0) — see `reachableSkills` for why honouring it would break three of this
|
|
31
|
+
* module's own invariants.
|
|
28
32
|
*
|
|
29
33
|
* **The model moves too (`read_skill`).** Scoped `read_skill` bounds the model to
|
|
30
34
|
* `reachableSkills(cursor)` — the declared successors of where it stands, plus the
|
|
31
35
|
* entries. A pick the gate ACCEPTS moves the cursor exactly like a declared edge
|
|
32
|
-
* does (`ctx.pendingSkillPick`, honoured in `
|
|
36
|
+
* does (`ctx.pendingSkillPick`, honoured in `makeResolveCursor`), so what the gate
|
|
33
37
|
* allows and what actually takes effect are the same set. A declared edge that
|
|
34
38
|
* fires the same turn wins (`D1 > D2`, docs/design/skill-graph.md §4A.1) and the
|
|
35
39
|
* run emits `agentfootprint.skill.reroute_superseded` rather than leaving the
|
|
36
40
|
* model's answered-"activated" claim quietly unmet.
|
|
41
|
+
*
|
|
42
|
+
* **The resolver says WHY, it is not asked to be guessed.** `explainNextSkill(ctx)`
|
|
43
|
+
* returns the destination AND the winning clause (`CursorMove`), decided at the same
|
|
44
|
+
* `return`. The agent stamps it on `context.evaluated` as `cursorMove`, which is how
|
|
45
|
+
* `routeRecorder()` can tell a model pick from a declared edge that happens to point
|
|
46
|
+
* at the same skill (8.5.0 — before, the drawn build-time provenance was read as the
|
|
47
|
+
* per-hop cause, so a model pick was recorded under a declared edge's label).
|
|
37
48
|
*/
|
|
38
49
|
import type { Injection, InjectionContext } from './types.js';
|
|
39
50
|
import type { Embedder } from '../../memory/embedding/types.js';
|
|
@@ -53,45 +64,75 @@ export interface BuildOptions {
|
|
|
53
64
|
*/
|
|
54
65
|
readonly check?: GraphCheckMode;
|
|
55
66
|
}
|
|
67
|
+
/** Where a turn starts, in the object-literal (flat) form. */
|
|
68
|
+
export type SkillGraphStart = string | {
|
|
69
|
+
readonly use: string;
|
|
70
|
+
} | {
|
|
71
|
+
readonly rules: ReadonlyArray<{
|
|
72
|
+
readonly when: (ctx: InjectionContext) => boolean;
|
|
73
|
+
readonly use: string;
|
|
74
|
+
}>;
|
|
75
|
+
} | {
|
|
76
|
+
readonly entries: readonly string[];
|
|
77
|
+
/** Rank the entries with a scorer strategy (`keywordScorer()`,
|
|
78
|
+
* `embeddingScorer(e)`, or your own). Takes precedence over `byRelevance`. */
|
|
79
|
+
readonly scoredBy?: EntryScorer;
|
|
80
|
+
/** Sugar: rank the entries with an embedder (cosine/softmax). Omit both → the
|
|
81
|
+
* LLM reads the menu and picks (`.entryByRead()`) — no model call. */
|
|
82
|
+
readonly byRelevance?: Embedder;
|
|
83
|
+
};
|
|
84
|
+
/** One tool-result transition in the object-literal (flat) form. */
|
|
85
|
+
export interface SkillGraphStep {
|
|
86
|
+
readonly from: string;
|
|
87
|
+
readonly to: string;
|
|
88
|
+
readonly when?: SkillRouteOptions['when'];
|
|
89
|
+
readonly onToolReturn?: string | RegExp;
|
|
90
|
+
readonly label?: string;
|
|
91
|
+
}
|
|
56
92
|
/**
|
|
57
|
-
* Object-literal form
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* skills that appear in an edge). Compiles to the SAME `SkillGraph`. `check`
|
|
61
|
-
* defaults to `'throw'` here (a new surface, fail-loud).
|
|
93
|
+
* Object-literal form, FLAT arm — `start` + `steps` declare the routing.
|
|
94
|
+
* `tree` is typed `never` here so `{ tree, start }` is a COMPILE error, not just
|
|
95
|
+
* a build-time refusal (see `SkillGraphConfig`).
|
|
62
96
|
*/
|
|
63
|
-
export interface
|
|
97
|
+
export interface SkillGraphFlatConfig {
|
|
64
98
|
/** Every skill in the graph (wired or not). */
|
|
65
99
|
readonly skills: readonly Injection[];
|
|
66
|
-
/** Where a turn starts.
|
|
67
|
-
readonly start?:
|
|
68
|
-
readonly use: string;
|
|
69
|
-
} | {
|
|
70
|
-
readonly rules: ReadonlyArray<{
|
|
71
|
-
readonly when: (ctx: InjectionContext) => boolean;
|
|
72
|
-
readonly use: string;
|
|
73
|
-
}>;
|
|
74
|
-
} | {
|
|
75
|
-
readonly entries: readonly string[];
|
|
76
|
-
/** Rank the entries with a scorer strategy (`keywordScorer()`,
|
|
77
|
-
* `embeddingScorer(e)`, or your own). Takes precedence over `byRelevance`. */
|
|
78
|
-
readonly scoredBy?: EntryScorer;
|
|
79
|
-
/** Sugar: rank the entries with an embedder (cosine/softmax). Omit both → the
|
|
80
|
-
* LLM reads the menu and picks (`.entryByRead()`) — no model call. */
|
|
81
|
-
readonly byRelevance?: Embedder;
|
|
82
|
-
};
|
|
100
|
+
/** Where a turn starts. */
|
|
101
|
+
readonly start?: SkillGraphStart;
|
|
83
102
|
/** Tool-result transitions; `from`/`to` are skill ids resolved against `skills`. */
|
|
84
|
-
readonly steps?:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
103
|
+
readonly steps?: readonly SkillGraphStep[];
|
|
104
|
+
readonly tree?: never;
|
|
105
|
+
readonly check?: GraphCheckMode;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Object-literal form, TREE arm — a decision tree owns the routing, so there is
|
|
109
|
+
* no entry menu and no cursor for `start`/`steps` to describe. Both are typed
|
|
110
|
+
* `never` so the contradiction is a compile error.
|
|
111
|
+
*/
|
|
112
|
+
export interface SkillGraphTreeConfig {
|
|
113
|
+
/** Every skill in the graph. Under `tree` this must be exactly the leaf set —
|
|
114
|
+
* a listed skill that is not a leaf would never load, and is refused. */
|
|
115
|
+
readonly skills: readonly Injection[];
|
|
91
116
|
/** A decision tree (instead of `start` + `steps`). */
|
|
92
|
-
readonly tree
|
|
117
|
+
readonly tree: DecisionNode | Injection;
|
|
118
|
+
readonly start?: never;
|
|
119
|
+
readonly steps?: never;
|
|
93
120
|
readonly check?: GraphCheckMode;
|
|
94
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Object-literal form of a skill graph — an alternative to the fluent builder.
|
|
124
|
+
* Listing `skills` INDEPENDENTLY of the wiring is the point: the check-up can then
|
|
125
|
+
* flag a skill that was listed but never wired (the fluent builder only ever sees
|
|
126
|
+
* skills that appear in an edge). Compiles to the SAME `SkillGraph`. `check`
|
|
127
|
+
* defaults to `'throw'` here (a new surface, fail-loud).
|
|
128
|
+
*
|
|
129
|
+
* A UNION of two arms, because `tree` and `start`/`steps` are two ways to declare
|
|
130
|
+
* the same thing and only one of them compiles: `{ tree, start }` is a type error
|
|
131
|
+
* for a TypeScript consumer and a build-time refusal for everyone else (8.4.0 —
|
|
132
|
+
* before, the tree silently won and the flat wiring was discarded). Valid tree-only
|
|
133
|
+
* and flat-only configs typecheck exactly as they did.
|
|
134
|
+
*/
|
|
135
|
+
export type SkillGraphConfig = SkillGraphFlatConfig | SkillGraphTreeConfig;
|
|
95
136
|
export type { EntryScore, EntryScoring };
|
|
96
137
|
/** Deterministic routing into a skill, keyed on the last tool result. */
|
|
97
138
|
export interface SkillRouteOptions {
|
|
@@ -122,8 +163,15 @@ export interface TreeOptions {
|
|
|
122
163
|
* A decision tree routes to EXACTLY ONE skill per iteration, so each leaf is
|
|
123
164
|
* stamped `autoActivate: 'currentSkill'` — its `inject.tools` reach the LLM
|
|
124
165
|
* ONLY when the tree routes there, instead of every skill's tools landing in
|
|
125
|
-
* the always-on static registry on every call.
|
|
126
|
-
*
|
|
166
|
+
* the always-on static registry on every call.
|
|
167
|
+
*
|
|
168
|
+
* `read_skill` cannot reach another LEAF mid-run (8.5.0): a tree has no cursor to
|
|
169
|
+
* move, and this "exactly one leaf" property is one of the reasons — admitting a
|
|
170
|
+
* second leaf would put two leaves' tools on the wire and make the dev-mode
|
|
171
|
+
* exactly-one monitor warn. It said otherwise until 8.5.0, and the pick was
|
|
172
|
+
* accepted and then silently dropped. The escape hatch is a skill registered
|
|
173
|
+
* BESIDE the graph (`.skill(x)`, `.selfExplain()`), which really does activate by
|
|
174
|
+
* `read_skill` and is admitted from anywhere.
|
|
127
175
|
*
|
|
128
176
|
* Default `true`. Set `false` for the legacy additive behavior (all leaves'
|
|
129
177
|
* tools always visible). A leaf that sets its OWN `autoActivate` in
|
|
@@ -158,6 +206,33 @@ export interface DecisionNode {
|
|
|
158
206
|
* other `decideSkill(...)` results. (Renamed from `decide` in v7 to avoid
|
|
159
207
|
* colliding with footprintjs's `decide()`.) */
|
|
160
208
|
export declare function decideSkill(predicate: (ctx: InjectionContext) => boolean, whenTrue: DecisionNode | Injection, whenFalse: DecisionNode | Injection, label?: string): DecisionNode;
|
|
209
|
+
/**
|
|
210
|
+
* WHY the cursor landed where it did on one iteration — the winning clause of the
|
|
211
|
+
* one cursor resolver, reported rather than guessed (8.5.0).
|
|
212
|
+
*
|
|
213
|
+
* • `'entry'` — cold start: the first entry whose `when` passed;
|
|
214
|
+
* • `'route'` — a declared, `from`-gated edge fired (D1);
|
|
215
|
+
* • `'model-pick'` — no declared edge fired, so the model's gate-accepted
|
|
216
|
+
* `read_skill` pick moved the cursor (D2), at cold start or mid-run;
|
|
217
|
+
* • `'stay'` — nothing fired; the cursor is sticky and stayed put;
|
|
218
|
+
* • `'none'` — no cursor at all (cold start with nothing to enter, or a
|
|
219
|
+
* decision `tree()`, which routes by predicate and has no cursor).
|
|
220
|
+
*
|
|
221
|
+
* This exists because the DRAWN provenance on a skill (`metadata.skillGraph`) answers
|
|
222
|
+
* "how is this skill reachable" — a build-time fact — and was being read as "how did
|
|
223
|
+
* we get here this turn". A model pick into a skill that also has a declared edge was
|
|
224
|
+
* therefore attributed to that edge, label and all, in the recorded route.
|
|
225
|
+
*/
|
|
226
|
+
export type CursorMoveCause = 'entry' | 'route' | 'model-pick' | 'stay' | 'none';
|
|
227
|
+
/** The cursor resolver's full answer: where, and by which clause. */
|
|
228
|
+
export interface CursorMove {
|
|
229
|
+
/** The cursor after this iteration (what `nextSkill` returns). */
|
|
230
|
+
readonly to?: string;
|
|
231
|
+
/** The cursor before it. */
|
|
232
|
+
readonly from?: string;
|
|
233
|
+
/** The winning clause. */
|
|
234
|
+
readonly by: CursorMoveCause;
|
|
235
|
+
}
|
|
161
236
|
/** A node in the drawn graph — a `predicate` diamond or a `skill` box. */
|
|
162
237
|
export interface SkillNode {
|
|
163
238
|
readonly id: string;
|
|
@@ -226,6 +301,19 @@ export interface SkillGraph {
|
|
|
226
301
|
* by predicate (no cursor) and returns the unchanged `ctx.currentSkillId`.
|
|
227
302
|
*/
|
|
228
303
|
nextSkill(ctx: InjectionContext): string | undefined;
|
|
304
|
+
/**
|
|
305
|
+
* The same answer as `nextSkill`, plus WHICH CLAUSE produced it — the resolver
|
|
306
|
+
* reporting its own reasoning instead of a consumer inferring it from the drawn
|
|
307
|
+
* provenance (8.5.0). `explainNextSkill(ctx).to === nextSkill(ctx)`, always: there
|
|
308
|
+
* is one resolver and `nextSkill` is a thin projection of this one, so the two can
|
|
309
|
+
* never drift.
|
|
310
|
+
*
|
|
311
|
+
* The agent threads this into the injection engine, which stamps the result on
|
|
312
|
+
* `agentfootprint.context.evaluated` as `cursorMove` — that is what lets
|
|
313
|
+
* `routeRecorder()` mark a model-pick hop as a model pick instead of borrowing the
|
|
314
|
+
* label of a declared edge that never fired.
|
|
315
|
+
*/
|
|
316
|
+
explainNextSkill(ctx: InjectionContext): CursorMove;
|
|
229
317
|
/**
|
|
230
318
|
* The REACHABLE set — which skills the model may `read_skill`-jump to from the
|
|
231
319
|
* current cursor. The agent's runtime gate rejects any `read_skill('id')` whose
|
|
@@ -233,8 +321,19 @@ export interface SkillGraph {
|
|
|
233
321
|
* • cold start (`currentSkillId` undefined) → the entry skills;
|
|
234
322
|
* • otherwise → the current skill's direct successors ∪ the entry skills, minus
|
|
235
323
|
* the current skill itself (deliberate "stay" is the no-tool-call ReAct stop).
|
|
236
|
-
*
|
|
237
|
-
*
|
|
324
|
+
*
|
|
325
|
+
* A decision `tree()` returns EMPTY (8.5.0). A tree routes by predicate on every
|
|
326
|
+
* iteration and has no cursor to jump: its leaves compile to `rule` triggers, and a
|
|
327
|
+
* `read_skill` call writes only `activatedInjectionIds`, which a `rule` trigger does
|
|
328
|
+
* not read. Until 8.5.0 this returned all the leaves, so the gate accepted a leaf
|
|
329
|
+
* pick, the tool answered "activated for the next iteration", and the leaf never
|
|
330
|
+
* activated. That is the same clause 8.4.0 already applies everywhere else — a skill
|
|
331
|
+
* is open only when its trigger is `llm-activated` — reaching the one set that had
|
|
332
|
+
* escaped it. The escape hatch under a tree is the OPEN skills (anything registered
|
|
333
|
+
* beside the graph: `.skill(x)`, `.selfExplain()`), which the agent's gate still
|
|
334
|
+
* admits from any cursor.
|
|
335
|
+
*
|
|
336
|
+
* Pure + deterministic.
|
|
238
337
|
*/
|
|
239
338
|
reachableSkills(currentSkillId?: string): readonly string[];
|
|
240
339
|
/**
|