agentfootprint 9.86.0 → 9.86.1
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/CHANGELOG.md +179 -8
- package/canonical-notes.json +1 -1
- package/dist/core/agent/coverage/ledger.js +8 -2
- package/dist/core/agent/coverage/ledger.js.map +1 -1
- package/dist/core/agent/stages/toolCalls.js +109 -20
- package/dist/core/agent/stages/toolCalls.js.map +1 -1
- package/dist/core/agent/stages/wrapUp.js +15 -4
- package/dist/core/agent/stages/wrapUp.js.map +1 -1
- package/dist/core/runbook/recording.js +3 -2
- package/dist/core/runbook/recording.js.map +1 -1
- package/dist/esm/core/agent/coverage/ledger.d.ts +5 -0
- package/dist/esm/core/agent/coverage/ledger.js +8 -2
- package/dist/esm/core/agent/coverage/ledger.js.map +1 -1
- package/dist/esm/core/agent/stages/toolCalls.d.ts +16 -2
- package/dist/esm/core/agent/stages/toolCalls.js +109 -20
- package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
- package/dist/esm/core/agent/stages/wrapUp.d.ts +11 -0
- package/dist/esm/core/agent/stages/wrapUp.js +15 -4
- package/dist/esm/core/agent/stages/wrapUp.js.map +1 -1
- package/dist/esm/core/agent/types.d.ts +5 -4
- package/dist/esm/core/runbook/recording.js +3 -2
- package/dist/esm/core/runbook/recording.js.map +1 -1
- package/dist/esm/events/payloads.d.ts +8 -0
- package/dist/esm/lib/injection-engine/skillSteps.js +6 -2
- package/dist/esm/lib/injection-engine/skillSteps.js.map +1 -1
- package/dist/esm/lib/trace-toolpack/traceToolpack.js +21 -14
- package/dist/esm/lib/trace-toolpack/traceToolpack.js.map +1 -1
- package/dist/lib/injection-engine/skillSteps.js +6 -2
- package/dist/lib/injection-engine/skillSteps.js.map +1 -1
- package/dist/lib/trace-toolpack/traceToolpack.js +21 -14
- package/dist/lib/trace-toolpack/traceToolpack.js.map +1 -1
- package/dist/types/core/agent/coverage/ledger.d.ts +5 -0
- package/dist/types/core/agent/coverage/ledger.d.ts.map +1 -1
- package/dist/types/core/agent/stages/toolCalls.d.ts +16 -2
- package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
- package/dist/types/core/agent/stages/wrapUp.d.ts +11 -0
- package/dist/types/core/agent/stages/wrapUp.d.ts.map +1 -1
- package/dist/types/core/agent/types.d.ts +5 -4
- package/dist/types/core/agent/types.d.ts.map +1 -1
- package/dist/types/core/runbook/recording.d.ts.map +1 -1
- package/dist/types/events/payloads.d.ts +8 -0
- package/dist/types/events/payloads.d.ts.map +1 -1
- package/dist/types/lib/injection-engine/skillSteps.d.ts.map +1 -1
- package/dist/types/lib/trace-toolpack/traceToolpack.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
*/
|
|
45
45
|
import type { PausableHandler, TypedScope } from 'footprintjs';
|
|
46
46
|
import type { PermissionChecker } from '../../../adapters/types.js';
|
|
47
|
+
import { type TurnRoute } from '../../../lib/injection-engine/routingPolicy.js';
|
|
47
48
|
import { type SkillTargetClass } from '../../../lib/injection-engine/skillGraph.js';
|
|
48
49
|
import { type SpokenIds } from '../../../lib/spokenIds.js';
|
|
49
50
|
import type { ToolProvider } from '../../../tool-providers/types.js';
|
|
@@ -563,7 +564,20 @@ export declare function composeReadSkillRefusal(args: {
|
|
|
563
564
|
* `held: true` with nothing named says a menu WAS outstanding and none of it
|
|
564
565
|
* may be named, which is a third sentence, not the no-menu one. */
|
|
565
566
|
readonly menuOffered?: SpokenIds;
|
|
566
|
-
/** How the turn's start was resolved
|
|
567
|
-
|
|
567
|
+
/** How the turn's start was resolved — `TurnRoute.by` — for the
|
|
568
|
+
* `'guard'`-without-a-menu arm. Each value is a different past fact about
|
|
569
|
+
* the turn, and the arm says only the one it was handed (9.86.1): it used to
|
|
570
|
+
* append "Declared routes moved the cursor instead." to every no-menu
|
|
571
|
+
* refusal, which is false for a cursor carried over from the previous turn
|
|
572
|
+
* (`'continuity'`) and for a menu the model's own pick had resolved
|
|
573
|
+
* (`'menu'`), and unprovable for `'none'`. */
|
|
574
|
+
readonly turnStartedBy?: TurnRoute['by'];
|
|
575
|
+
/** A cursor EXISTED on that call and the role filter kept its id out of the
|
|
576
|
+
* sentence (9.86.1). `cursorId` is absent in two situations that need two
|
|
577
|
+
* different sentences: a cold start, where "the turn's start" is the true
|
|
578
|
+
* anchor, and a hidden cursor, where the skill is real and merely unnamed.
|
|
579
|
+
* The description already withholds a hidden cursor's name — pinned by
|
|
580
|
+
* `test/security/skill-visibility.test.ts` — and the refusal printed it raw. */
|
|
581
|
+
readonly cursorWithheld?: boolean;
|
|
568
582
|
}): string;
|
|
569
583
|
export declare function buildToolCallsHandler(deps: ToolCallsHandlerDeps): PausableHandler<TypedScope<AgentState>>;
|
|
@@ -271,7 +271,7 @@ export function unknownToolResult(toolName, dispatchable) {
|
|
|
271
271
|
* composers rather than re-deriving what it says.
|
|
272
272
|
*/
|
|
273
273
|
export function composeReadSkillRefusal(args) {
|
|
274
|
-
const { requestedId, targetClass, cursorId, hops, openIds, isTree, posture, menuOffered,
|
|
274
|
+
const { requestedId, targetClass, cursorId, hops, openIds, isTree, posture, menuOffered, turnStartedBy, cursorWithheld, } = args;
|
|
275
275
|
const head = `read_skill("${requestedId}") was not granted on that call: `;
|
|
276
276
|
// Open skills are the one list every arm may name: they are admitted from
|
|
277
277
|
// every cursor and no posture governs them (the posture arm below is reached
|
|
@@ -302,10 +302,23 @@ export function composeReadSkillRefusal(args) {
|
|
|
302
302
|
return (`${head}this graph's 'guard' posture admits a routing pick only from the menu the ` +
|
|
303
303
|
`framework offered, and '${requestedId}' was not admitted on that call.${openClause}`);
|
|
304
304
|
}
|
|
305
|
+
// What the gate KNOWS about why no menu was outstanding is `TurnRoute.by`,
|
|
306
|
+
// and each value is a different finished fact. Nothing is asserted about
|
|
307
|
+
// how the cursor came to be where it was beyond that — the old tail,
|
|
308
|
+
// "Declared routes moved the cursor instead.", was composed for every
|
|
309
|
+
// value and was true of none of them in particular.
|
|
310
|
+
const startClause = turnStartedBy === 'entry' || turnStartedBy === 'intent'
|
|
311
|
+
? " — the turn's start had already been resolved decisively"
|
|
312
|
+
: turnStartedBy === 'continuity'
|
|
313
|
+
? ' — the cursor had been carried over from the previous turn'
|
|
314
|
+
: turnStartedBy === 'menu'
|
|
315
|
+
? ' — the menu had already been resolved by an earlier pick'
|
|
316
|
+
: turnStartedBy === 'decider'
|
|
317
|
+
? " — the menu had been resolved by the configured decider before the turn's first call"
|
|
318
|
+
: '';
|
|
305
319
|
return (`${head}this graph's 'guard' posture admits a routing pick only while the framework ` +
|
|
306
320
|
`has declared ambiguity, and no menu was outstanding when that call was made` +
|
|
307
|
-
`${
|
|
308
|
-
`. Declared routes moved the cursor instead.${openClause}`);
|
|
321
|
+
`${startClause}.${openClause}`);
|
|
309
322
|
}
|
|
310
323
|
if (isTree === true) {
|
|
311
324
|
return (`${head}this map is a decision tree. A tree routes by predicate on every iteration ` +
|
|
@@ -316,7 +329,14 @@ export function composeReadSkillRefusal(args) {
|
|
|
316
329
|
// this arm — 'self' is answered by the notice, 'hop' and 'open' are admitted
|
|
317
330
|
// — and it is carried rather than re-derived so the sentence and the verdict
|
|
318
331
|
// read the same classification.
|
|
319
|
-
|
|
332
|
+
// Three anchors, not two: a named cursor, a cursor this caller may not be
|
|
333
|
+
// told the name of, and a genuine cold start. The middle one used to print
|
|
334
|
+
// the raw id — the one name the description's own law withholds.
|
|
335
|
+
const from = cursorId !== undefined
|
|
336
|
+
? `'${cursorId}'`
|
|
337
|
+
: cursorWithheld === true
|
|
338
|
+
? 'the skill the cursor stood in'
|
|
339
|
+
: "the turn's start";
|
|
320
340
|
// The gate reaches this arm only with `'unreachable'`; the weaker sentence is
|
|
321
341
|
// for a caller that refused an admissible class for a reason of its own, and
|
|
322
342
|
// it is deliberately not an invented explanation of one.
|
|
@@ -925,6 +945,11 @@ export function buildToolCallsHandler(deps) {
|
|
|
925
945
|
}
|
|
926
946
|
const currentSkillId = scope.currentSkillId;
|
|
927
947
|
const hops = deps.allowedSkillIds(currentSkillId);
|
|
948
|
+
const targetClass = classifySkillTarget({
|
|
949
|
+
...(currentSkillId !== undefined && { cursor: currentSkillId }),
|
|
950
|
+
target,
|
|
951
|
+
hops,
|
|
952
|
+
});
|
|
928
953
|
// ── A PROPOSAL TO THE CURSOR'S OWN SKILL IS A STAY (9.86.0) ──────
|
|
929
954
|
// `makeReachableSkills` filters the cursor out of its own successor
|
|
930
955
|
// set — right for a MOVE, and this judge read it as "not reachable"
|
|
@@ -937,11 +962,47 @@ export function buildToolCallsHandler(deps) {
|
|
|
937
962
|
// The event carries the existing `'accepted'` outcome plus an
|
|
938
963
|
// ADDITIVE `stay: true`, deliberately not a new enum member: an
|
|
939
964
|
// exhaustive switch over `outcome` in a consumer must keep compiling.
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
965
|
+
//
|
|
966
|
+
// A STAY COMPETES FOR THE SLOT LIKE ANY OTHER ACCEPTED PROPOSAL
|
|
967
|
+
// (9.86.1). The batch law is "first ACCEPTED proposal wins, later
|
|
968
|
+
// proposals to OTHER targets are superseded", and a stay is accepted
|
|
969
|
+
// — so a stay judged first holds the slot (with nothing written to
|
|
970
|
+
// `pendingToolTransition`, because a stay moves nothing) and a later
|
|
971
|
+
// sibling proposing a hop is `'superseded'` and lands in the
|
|
972
|
+
// `route_conflict` losers, exactly as two conflicting hops do. Before
|
|
973
|
+
// this the stay `continue`d past the bookkeeping, so the tool that
|
|
974
|
+
// judged its data first lost to whichever sibling came second, with
|
|
975
|
+
// two `'accepted'` events in one batch and no conflict on the record.
|
|
976
|
+
// The same law in the other order: a stay proposed AFTER an accepted
|
|
977
|
+
// hop is superseded by it, not accepted beside it.
|
|
978
|
+
if (targetClass === 'self') {
|
|
979
|
+
if (state.winner !== undefined && state.winner.targetSkillId !== target) {
|
|
980
|
+
state.losers.push({
|
|
981
|
+
toolCallId: call.toolCallId,
|
|
982
|
+
toolName: call.toolName,
|
|
983
|
+
target,
|
|
984
|
+
});
|
|
985
|
+
typedEmit(scope, 'agentfootprint.tools.effect', {
|
|
986
|
+
kind: 'propose-transition',
|
|
987
|
+
outcome: 'superseded',
|
|
988
|
+
toolName: call.toolName,
|
|
989
|
+
toolCallId: call.toolCallId,
|
|
990
|
+
iteration: call.iteration,
|
|
991
|
+
targetSkillId: target,
|
|
992
|
+
reason: effect.reason,
|
|
993
|
+
supersededBy: 'earlier-proposal',
|
|
994
|
+
});
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
if (state.winner === undefined) {
|
|
998
|
+
state.winner = {
|
|
999
|
+
targetSkillId: target,
|
|
1000
|
+
toolName: call.toolName,
|
|
1001
|
+
toolCallId: call.toolCallId,
|
|
1002
|
+
reason: effect.reason,
|
|
1003
|
+
iteration: call.iteration,
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
945
1006
|
typedEmit(scope, 'agentfootprint.tools.effect', {
|
|
946
1007
|
kind: 'propose-transition',
|
|
947
1008
|
outcome: 'accepted',
|
|
@@ -954,10 +1015,25 @@ export function buildToolCallsHandler(deps) {
|
|
|
954
1015
|
});
|
|
955
1016
|
continue;
|
|
956
1017
|
}
|
|
957
|
-
if (
|
|
1018
|
+
if (targetClass === 'unreachable') {
|
|
1019
|
+
// ── THE REFUSAL SPEAKS WITH THE FILTERED SETS (9.86.1) ──────────
|
|
1020
|
+
// This sentence is appended to the tool result the model reads and
|
|
1021
|
+
// rides the `tools.effect` payload, and it named the raw hop set and
|
|
1022
|
+
// the raw cursor — the leak the `read_skill` refusals closed one
|
|
1023
|
+
// function up. Same filter, same law: the hidden set the tools slot
|
|
1024
|
+
// resolved for this iteration, and OMIT, NEVER DENY — a hop set the
|
|
1025
|
+
// filter emptied composes no reachable clause rather than an empty
|
|
1026
|
+
// one. Admission is still judged on the raw `hops`.
|
|
1027
|
+
const hiddenIds = new Set(scope.hiddenSkillIds ?? []);
|
|
1028
|
+
const mayName = (id) => !hiddenIds.has(id);
|
|
1029
|
+
const reachable = spoken(hops, mayName);
|
|
1030
|
+
const from = currentSkillId === undefined
|
|
1031
|
+
? 'the turn start'
|
|
1032
|
+
: mayName(currentSkillId)
|
|
1033
|
+
? `'${currentSkillId}'`
|
|
1034
|
+
: 'the skill the cursor stood in';
|
|
958
1035
|
refuse('propose-transition', `propose-transition → '${target}' was refused: '${target}' is not reachable from ` +
|
|
959
|
-
`${
|
|
960
|
-
`the graph's own law${hops.length > 0 ? ` (reachable: ${hops.join(', ')})` : ''}. The graph decides — a proposal is evidence, never authority.`, { targetSkillId: target, reason: effect.reason });
|
|
1036
|
+
`${from} per the graph's own law${reachable.named.length > 0 ? ` (reachable: ${reachable.named.join(', ')})` : ''}. The graph decides — a proposal is evidence, never authority.`, { targetSkillId: target, reason: effect.reason });
|
|
961
1037
|
continue;
|
|
962
1038
|
}
|
|
963
1039
|
if (state.winner !== undefined && state.winner.targetSkillId !== target) {
|
|
@@ -2779,6 +2855,15 @@ export function buildToolCallsHandler(deps) {
|
|
|
2779
2855
|
// "nothing reachable may be named", and it used to assert the first.
|
|
2780
2856
|
const hopsSpoken = spoken(hops, mayName);
|
|
2781
2857
|
const openSpoken = spoken(openAll, mayName);
|
|
2858
|
+
// The cursor through the SAME filter (9.86.1). The description
|
|
2859
|
+
// withholds a hidden cursor's name and every refusal below printed
|
|
2860
|
+
// it raw in two clauses; `cursorWithheld` keeps the sentence's
|
|
2861
|
+
// anchor honest ("the skill the cursor stood in") without the id.
|
|
2862
|
+
const cursorSpoken = currentSkillId === undefined
|
|
2863
|
+
? {}
|
|
2864
|
+
: mayName(currentSkillId)
|
|
2865
|
+
? { cursorId: currentSkillId }
|
|
2866
|
+
: { cursorWithheld: true };
|
|
2782
2867
|
const hopsNamed = hopsSpoken.named;
|
|
2783
2868
|
const openNamed = openSpoken.named;
|
|
2784
2869
|
// The ONE owner of "is this target the cursor?" — the same function
|
|
@@ -2896,7 +2981,7 @@ export function buildToolCallsHandler(deps) {
|
|
|
2896
2981
|
result = composeReadSkillRefusal({
|
|
2897
2982
|
requestedId: reqId,
|
|
2898
2983
|
targetClass,
|
|
2899
|
-
...
|
|
2984
|
+
...cursorSpoken,
|
|
2900
2985
|
hops: hopsSpoken,
|
|
2901
2986
|
openIds: openSpoken,
|
|
2902
2987
|
...(deps.skillGraphIsTree === true && { isTree: true }),
|
|
@@ -2933,7 +3018,7 @@ export function buildToolCallsHandler(deps) {
|
|
|
2933
3018
|
result = composeReadSkillRefusal({
|
|
2934
3019
|
requestedId: reqId,
|
|
2935
3020
|
targetClass,
|
|
2936
|
-
...
|
|
3021
|
+
...cursorSpoken,
|
|
2937
3022
|
// No hop is named under a posture, and the graph's hop set
|
|
2938
3023
|
// is not this arm's subject — `held: false` is the honest
|
|
2939
3024
|
// shape, not a claim that nothing was reachable (this arm
|
|
@@ -2942,9 +3027,9 @@ export function buildToolCallsHandler(deps) {
|
|
|
2942
3027
|
openIds: openSpoken,
|
|
2943
3028
|
posture: deps.skillStrictness,
|
|
2944
3029
|
...(menuNamed !== undefined && { menuOffered: menuNamed }),
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
}),
|
|
3030
|
+
// The whole verdict, not a boolean derived from two of its
|
|
3031
|
+
// six values: the composer says one past fact per value.
|
|
3032
|
+
...(turnRoute !== undefined && { turnStartedBy: turnRoute.by }),
|
|
2948
3033
|
});
|
|
2949
3034
|
typedEmit(scope, 'agentfootprint.skill.rejected', {
|
|
2950
3035
|
requestedId: reqId,
|
|
@@ -3390,10 +3475,14 @@ export function buildToolCallsHandler(deps) {
|
|
|
3390
3475
|
// silently invents a decision, for the same reason it never silently
|
|
3391
3476
|
// drops one.
|
|
3392
3477
|
error = true;
|
|
3478
|
+
// A past fact about the resumed call, not a forecast about the
|
|
3479
|
+
// turn (9.86.1): "cannot be retried this turn … Answer without it,
|
|
3480
|
+
// or finish" was a prediction plus a standing order on a result
|
|
3481
|
+
// that is re-read on every later call.
|
|
3393
3482
|
result =
|
|
3394
|
-
`tool '${toolName}' was not executed
|
|
3395
|
-
`
|
|
3396
|
-
`
|
|
3483
|
+
`tool '${toolName}' was not executed on that call, and the resumed dispatch had ` +
|
|
3484
|
+
`no second checkpoint to retry it on: it declares its own checkIn consent gate, ` +
|
|
3485
|
+
`and that gate tripped for those arguments. (To ` +
|
|
3397
3486
|
`the agent's author: the middleware '${askedBy}' and the tool's checkIn ask ` +
|
|
3398
3487
|
`different questions — one is the rule's, one is the tool's with the evidence ` +
|
|
3399
3488
|
`pack attached — so approving one is not answering the other. Keep one gate for ` +
|