portable-agent-layer 0.85.19 → 0.85.21
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.
|
@@ -88,11 +88,11 @@
|
|
|
88
88
|
},
|
|
89
89
|
"claimCheck": {
|
|
90
90
|
"type": "boolean",
|
|
91
|
-
"description": "At each stop, check whether the final reply claims a result (tests pass, CI green, works now) that no command in that turn could have shown, and log the verdict to memory/signals/claim-checks/. Stores the claiming sentence only when nothing backed it."
|
|
91
|
+
"description": "At each stop, check whether the final reply claims a result (tests pass, CI green, works now) or a status (not pushed, merged, live) that no command in that turn could have shown, and log the verdict to memory/signals/claim-checks/. A claim about what is committed, pushed, merged or released is backed only by a git or gh command. Stores the claiming sentence only when nothing backed it."
|
|
92
92
|
},
|
|
93
93
|
"claimCheckBlocks": {
|
|
94
94
|
"type": "boolean",
|
|
95
|
-
"description": "Opt-in, off by default. Send a reply with an unbacked result claim back to the agent once, asking it to show the output or say the result is unverified. Needs claimCheck. Not on Cursor, whose stop payload carries no reply text."
|
|
95
|
+
"description": "Opt-in, off by default. Send a reply with an unbacked result or status claim back to the agent once, asking it to show the output or say the result is unverified. Needs claimCheck. Not on Cursor, whose stop payload carries no reply text."
|
|
96
96
|
},
|
|
97
97
|
"steeringTestReport": { "type": "boolean", "description": "When true, the assistant notes to the user which steering self-check fired. For the dual-live test period; ships false." }
|
|
98
98
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Whether a reply claims a result (tests pass, CI green, it works now)
|
|
3
|
-
* command in the same turn could have shown.
|
|
4
|
-
*
|
|
2
|
+
* Whether a reply claims a result (tests pass, CI green, it works now) or a status
|
|
3
|
+
* (not pushed, merged, live) that no command in the same turn could have shown.
|
|
4
|
+
* Precision over recall: a hedged sentence or a question is never a claim, and an
|
|
5
|
+
* unreadable turn is never unbacked.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { isSystemText } from "./prompt-text";
|
|
@@ -12,6 +13,17 @@ const CLAIM_RE =
|
|
|
12
13
|
const HEDGED_RE =
|
|
13
14
|
/\b(?:not|no longer|fail(?:s|ed|ing)?|until|when|if|unless|should|would|will|might|could|may)\b|n't\b/i;
|
|
14
15
|
|
|
16
|
+
const stateClaim = (states: string) =>
|
|
17
|
+
new RegExp(
|
|
18
|
+
String.raw`(?:\b(?:is|are|was|were|been|not|yet|still|already|now)|n't|n’t|'s|’s)\s+(?:${states})\b|\bun(?:${states})\b`,
|
|
19
|
+
"i"
|
|
20
|
+
);
|
|
21
|
+
const VERSION_CONTROL_CLAIM_RE = stateClaim("committed|pushed|merged|released|tagged");
|
|
22
|
+
const DEPLOY_CLAIM_RE = stateClaim("deployed|live|installed|shipped");
|
|
23
|
+
const STATUS_HEDGED_RE =
|
|
24
|
+
/\b(?:once|until|when|if|unless|should|would|will|might|could|may)\b/i;
|
|
25
|
+
const READS_VERSION_CONTROL_RE = /\b(?:git|gh)\b/;
|
|
26
|
+
|
|
15
27
|
const COMMAND_TOOLS = new Set([
|
|
16
28
|
"Bash",
|
|
17
29
|
"BashOutput",
|
|
@@ -29,11 +41,12 @@ export interface ClaimCheck {
|
|
|
29
41
|
claims: string[];
|
|
30
42
|
}
|
|
31
43
|
|
|
32
|
-
function
|
|
44
|
+
function statements(reply: string): string[] {
|
|
33
45
|
return reply
|
|
34
46
|
.split(/(?<=[.!?])\s+|\n+/)
|
|
35
|
-
.map((s) => s.trim()
|
|
36
|
-
.filter(
|
|
47
|
+
.map((s) => s.trim())
|
|
48
|
+
.filter((s) => s && !s.endsWith("?"))
|
|
49
|
+
.map((s) => s.replace(/[.!]+$/, ""));
|
|
37
50
|
}
|
|
38
51
|
|
|
39
52
|
/** A quoted claim is being talked about, not made. */
|
|
@@ -42,16 +55,47 @@ function withoutQuotes(sentence: string): string {
|
|
|
42
55
|
}
|
|
43
56
|
|
|
44
57
|
export function resultClaims(reply: string): string[] {
|
|
45
|
-
return
|
|
58
|
+
return statements(reply).filter((s) => {
|
|
46
59
|
const own = withoutQuotes(s);
|
|
47
60
|
return CLAIM_RE.test(own) && !HEDGED_RE.test(own);
|
|
48
61
|
});
|
|
49
62
|
}
|
|
50
63
|
|
|
64
|
+
/** A checklist item states a goal and a line ending in a colon introduces one. */
|
|
65
|
+
function isGoalOrHeading(sentence: string): boolean {
|
|
66
|
+
return /^[-*]\s*\[[ xX]\]/.test(sentence) || /:\**$/.test(sentence);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isStatusClaim(sentence: string): boolean {
|
|
70
|
+
const own = withoutQuotes(sentence);
|
|
71
|
+
if (isGoalOrHeading(own)) return false;
|
|
72
|
+
return (
|
|
73
|
+
(VERSION_CONTROL_CLAIM_RE.test(own) || DEPLOY_CLAIM_RE.test(own)) &&
|
|
74
|
+
!STATUS_HEDGED_RE.test(own)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function statusClaims(reply: string): string[] {
|
|
79
|
+
return statements(reply).filter(isStatusClaim);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** What is committed, pushed, merged or released only git can say. */
|
|
83
|
+
export function isVersionControlClaim(claim: string): boolean {
|
|
84
|
+
return VERSION_CONTROL_CLAIM_RE.test(withoutQuotes(claim));
|
|
85
|
+
}
|
|
86
|
+
|
|
51
87
|
interface TranscriptLine {
|
|
52
88
|
type?: string;
|
|
53
89
|
message?: { content?: unknown };
|
|
54
|
-
payload?: {
|
|
90
|
+
payload?: {
|
|
91
|
+
type?: string;
|
|
92
|
+
role?: string;
|
|
93
|
+
name?: string;
|
|
94
|
+
content?: unknown;
|
|
95
|
+
input?: unknown;
|
|
96
|
+
arguments?: unknown;
|
|
97
|
+
action?: unknown;
|
|
98
|
+
};
|
|
55
99
|
}
|
|
56
100
|
|
|
57
101
|
function parsed(line: string): TranscriptLine | null {
|
|
@@ -62,7 +106,9 @@ function parsed(line: string): TranscriptLine | null {
|
|
|
62
106
|
}
|
|
63
107
|
}
|
|
64
108
|
|
|
65
|
-
function blocks(
|
|
109
|
+
function blocks(
|
|
110
|
+
content: unknown
|
|
111
|
+
): { type?: string; name?: string; text?: string; input?: unknown }[] {
|
|
66
112
|
return Array.isArray(content) ? content : [];
|
|
67
113
|
}
|
|
68
114
|
|
|
@@ -86,37 +132,51 @@ function isUserPrompt(entry: TranscriptLine): boolean {
|
|
|
86
132
|
return text !== null && !isSystemText(text) && !isCodexInjectedContext(text);
|
|
87
133
|
}
|
|
88
134
|
|
|
89
|
-
|
|
135
|
+
const ran = (input: unknown) => JSON.stringify(input ?? {});
|
|
136
|
+
|
|
137
|
+
function commandsIn(entry: TranscriptLine): string[] {
|
|
90
138
|
if (entry.type === "assistant")
|
|
91
|
-
return blocks(entry.message?.content)
|
|
92
|
-
(b) => b.type === "tool_use" && COMMAND_TOOLS.has(b.name ?? "")
|
|
93
|
-
|
|
139
|
+
return blocks(entry.message?.content)
|
|
140
|
+
.filter((b) => b.type === "tool_use" && COMMAND_TOOLS.has(b.name ?? ""))
|
|
141
|
+
.map((b) => ran(b.input));
|
|
94
142
|
const call = entry.payload;
|
|
95
|
-
if (entry.type !== "response_item" || !call) return
|
|
96
|
-
|
|
143
|
+
if (entry.type !== "response_item" || !call) return [];
|
|
144
|
+
const input = call.input ?? call.arguments ?? call.action;
|
|
145
|
+
if (call.type === "local_shell_call") return [ran(input)];
|
|
97
146
|
const isCall = call.type === "function_call" || call.type === "custom_tool_call";
|
|
98
|
-
return isCall && COMMAND_TOOLS.has(call.name ?? "") ?
|
|
147
|
+
return isCall && COMMAND_TOOLS.has(call.name ?? "") ? [ran(input)] : [];
|
|
99
148
|
}
|
|
100
149
|
|
|
101
150
|
function isTurnEntry(entry: TranscriptLine): boolean {
|
|
102
151
|
return ["user", "assistant", "response_item"].includes(entry.type ?? "");
|
|
103
152
|
}
|
|
104
153
|
|
|
105
|
-
/** Null when no line reads as a turn, so an
|
|
106
|
-
|
|
154
|
+
/** What each command this turn ran. Null when no line reads as a turn, so an
|
|
155
|
+
* unknown transcript format never counts as none. */
|
|
156
|
+
export function commandsThisTurn(lines: string[]): string[] | null {
|
|
107
157
|
const entries = lines.map(parsed).filter((e): e is TranscriptLine => e !== null);
|
|
108
158
|
if (!entries.some(isTurnEntry)) return null;
|
|
109
|
-
let commands =
|
|
159
|
+
let commands: string[] = [];
|
|
110
160
|
for (const entry of entries) {
|
|
111
|
-
if (isUserPrompt(entry)) commands =
|
|
112
|
-
commands
|
|
161
|
+
if (isUserPrompt(entry)) commands = [];
|
|
162
|
+
commands.push(...commandsIn(entry));
|
|
113
163
|
}
|
|
114
164
|
return commands;
|
|
115
165
|
}
|
|
116
166
|
|
|
117
|
-
|
|
118
|
-
|
|
167
|
+
function isBacked(claim: string, commands: string[]): boolean {
|
|
168
|
+
if (!isVersionControlClaim(claim)) return commands.length > 0;
|
|
169
|
+
return commands.some((command) => READS_VERSION_CONTROL_RE.test(command));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function checkClaims(reply: string, commands: string[] | null): ClaimCheck {
|
|
173
|
+
const claims = statements(reply).filter(
|
|
174
|
+
(s) => resultClaims(s).length > 0 || isStatusClaim(s)
|
|
175
|
+
);
|
|
119
176
|
if (claims.length === 0) return { verdict: "none", claims };
|
|
120
177
|
if (commands === null) return { verdict: "unknown", claims };
|
|
121
|
-
|
|
178
|
+
const unbacked = claims.filter((claim) => !isBacked(claim, commands));
|
|
179
|
+
return unbacked.length > 0
|
|
180
|
+
? { verdict: "unbacked", claims: unbacked }
|
|
181
|
+
: { verdict: "backed", claims };
|
|
122
182
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The claim check at stop: every reply that makes a result claim is logged to
|
|
2
|
+
* The claim check at stop: every reply that makes a result or status claim is logged to
|
|
3
3
|
* memory/signals/claim-checks/ with its verdict. The claiming sentence is kept
|
|
4
4
|
* only when no command backed it, so the log can be audited. Sending the reply
|
|
5
5
|
* back is opt-in (dynamicContext.claimCheckBlocks).
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type ClaimVerdict,
|
|
14
14
|
checkClaims,
|
|
15
15
|
commandsThisTurn,
|
|
16
|
+
isVersionControlClaim,
|
|
16
17
|
} from "./claim-check";
|
|
17
18
|
import { hookFinalReply, hookSessionId } from "./hook-turn";
|
|
18
19
|
import { ensureDir, paths } from "./paths";
|
|
@@ -85,11 +86,14 @@ function sendsBack(payload: StopTurnPayload, check: ClaimCheck): boolean {
|
|
|
85
86
|
|
|
86
87
|
function sendBackReason(claims: string[]): string {
|
|
87
88
|
const quoted = claims.map((c) => `"${c.slice(0, CLAIM_MAX)}"`).join(", ");
|
|
88
|
-
|
|
89
|
+
const ask = claims.some(isVersionControlClaim)
|
|
90
|
+
? "Read what is committed, pushed, merged or released with git or gh and show it"
|
|
91
|
+
: "Run the check and show its output";
|
|
92
|
+
return `Your reply claims what no command in this turn showed: ${quoted}. ${ask}, or say it is unverified.`;
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
/**
|
|
92
|
-
* Checks the final reply against the turn's commands and logs any
|
|
96
|
+
* Checks the final reply against the turn's commands and logs any claim.
|
|
93
97
|
* Returns the reason to send the reply back with, when that is opted in.
|
|
94
98
|
*/
|
|
95
99
|
export function watchClaims(
|
|
@@ -103,7 +107,8 @@ export function watchClaims(
|
|
|
103
107
|
const check = checkClaims(reply, commands);
|
|
104
108
|
if (check.verdict === "none") return null;
|
|
105
109
|
const blocked = sendsBack(payload, check);
|
|
106
|
-
|
|
110
|
+
const session = hookSessionId(payload) ?? "unknown";
|
|
111
|
+
record({ check, commands: commands?.length ?? null, session, blocked }, now);
|
|
107
112
|
return blocked ? sendBackReason(check.claims) : null;
|
|
108
113
|
}
|
|
109
114
|
|
|
@@ -208,7 +208,7 @@ export function claimCheckLines(records: ClaimRecord[]): string[] {
|
|
|
208
208
|
const sentBack = records.filter((r) => r.blocked).length;
|
|
209
209
|
return [
|
|
210
210
|
"",
|
|
211
|
-
`Result claims: ${plural(records.length, "reply", "replies")} · ${unbacked.length} with no command behind it · ${sentBack} sent back · ${unknown} unreadable`,
|
|
211
|
+
`Result and status claims: ${plural(records.length, "reply", "replies")} · ${unbacked.length} with no command behind it · ${sentBack} sent back · ${unknown} unreadable`,
|
|
212
212
|
...unbacked
|
|
213
213
|
.slice(-UNBACKED_LISTED)
|
|
214
214
|
.flatMap((r) => (r.claims ?? []).map((c) => ` no command: ${c}`)),
|