jev-agent-tools 0.1.4 → 0.3.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/CHANGELOG.md +106 -1
- package/CONTRIBUTING.md +43 -0
- package/README.md +58 -17
- package/SECURITY.md +43 -0
- package/dist/adapters/analysis-context.js +75 -0
- package/dist/adapters/ask-files.js +198 -0
- package/dist/adapters/ask-proof.js +200 -0
- package/dist/adapters/ask-syntax.js +385 -0
- package/dist/adapters/canonical-path.js +17 -0
- package/dist/adapters/command.js +234 -0
- package/dist/adapters/docs.js +192 -0
- package/dist/adapters/evidence-context.js +119 -0
- package/dist/adapters/exec.js +207 -0
- package/dist/adapters/files.js +418 -0
- package/dist/adapters/find.js +150 -0
- package/dist/adapters/git-base.js +32 -0
- package/dist/adapters/git-inventory.js +71 -0
- package/dist/adapters/git.js +483 -0
- package/dist/adapters/locate-file.js +197 -0
- package/dist/adapters/output-lines.js +46 -0
- package/dist/adapters/private-storage.js +106 -0
- package/dist/adapters/risk-callers.js +429 -0
- package/dist/adapters/runner-version.js +78 -0
- package/dist/adapters/shell.js +92 -0
- package/dist/adapters/syntax.js +187 -0
- package/dist/adapters/test-inventory.js +139 -0
- package/dist/adapters/usage.js +20 -0
- package/dist/adapters/utf8.js +47 -0
- package/dist/configuration.js +267 -0
- package/dist/constants.js +140 -0
- package/dist/core/ask-closure.js +282 -0
- package/dist/core/ask-proof.js +1 -0
- package/dist/core/ask-references.js +278 -0
- package/dist/core/asks.js +507 -0
- package/dist/core/batches.js +65 -0
- package/dist/core/command-output.js +224 -0
- package/dist/core/diff.js +178 -0
- package/dist/core/docs.js +302 -0
- package/dist/core/find.js +108 -0
- package/dist/core/git.js +1 -0
- package/dist/core/imports.js +550 -0
- package/dist/core/integrity.js +45 -0
- package/dist/core/lexical.js +132 -0
- package/dist/core/locate.js +169 -0
- package/dist/core/output.js +137 -0
- package/dist/core/pointer.js +29 -0
- package/dist/core/result-report.js +302 -0
- package/dist/core/risk-callers.js +851 -0
- package/dist/core/runner-version.js +45 -0
- package/dist/core/secret-path.js +34 -0
- package/dist/core/sections.js +230 -0
- package/dist/core/state.js +51 -0
- package/dist/core/syntax.js +1 -0
- package/dist/core/test-commands.js +334 -0
- package/dist/core/test-coverage.js +74 -0
- package/dist/core/test-discovery.js +1382 -0
- package/dist/core/test-evidence.js +527 -0
- package/dist/core/test-state.js +81 -0
- package/dist/core/truncate.js +12 -0
- package/dist/core/units.js +349 -0
- package/dist/describe.js +23 -0
- package/dist/guide.js +33 -0
- package/dist/host.js +24 -0
- package/dist/jev/client.js +456 -0
- package/dist/jev/pool.js +54 -0
- package/dist/jev/types.js +1 -0
- package/dist/mcp/main.js +124 -0
- package/dist/mcp/protocol.js +210 -0
- package/dist/mcp/tools.js +129 -0
- package/dist/presets/docs.js +62 -0
- package/dist/presets/risk.js +179 -0
- package/dist/presets/spec.js +81 -0
- package/dist/presets/witnesses.js +249 -0
- package/dist/render.js +114 -0
- package/dist/report-schema.js +1356 -0
- package/dist/result-types.js +1 -0
- package/dist/result.js +3 -0
- package/dist/runtime.js +1 -0
- package/dist/session.js +147 -0
- package/dist/texts/ask-files.js +3 -0
- package/dist/texts/ask.js +4 -0
- package/dist/texts/check-diff.js +20 -0
- package/dist/texts/configuration.js +1 -0
- package/dist/texts/find.js +19 -0
- package/dist/texts/guide.js +3 -0
- package/dist/texts/instructions.js +72 -0
- package/dist/texts/locate.js +15 -0
- package/dist/texts/select-tests.js +4 -0
- package/dist/tools/ask-files.js +450 -0
- package/dist/tools/ask-schema.js +70 -0
- package/dist/tools/ask.js +1147 -0
- package/dist/tools/check-diff.js +594 -0
- package/dist/tools/docs-check.js +408 -0
- package/dist/tools/find.js +682 -0
- package/dist/tools/locate.js +602 -0
- package/dist/tools/review-report.js +230 -0
- package/dist/tools/select-tests.js +821 -0
- package/dist/tools/spec-check.js +263 -0
- package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +31 -0
- package/docs/adr/0002-one-http-protocol-across-hosts.md +17 -0
- package/docs/adr/0003-explicit-scope-conservative-automation.md +19 -0
- package/docs/adr/0004-compiled-typed-intents.md +19 -0
- package/docs/adr/0005-evidence-construction-before-judgment.md +19 -0
- package/docs/adr/0006-visible-uncertainty-constrained-controls.md +21 -0
- package/docs/adr/0007-bounded-evidence-visible-limits.md +21 -0
- package/docs/adr/0008-static-test-discovery-conservative-plans.md +19 -0
- package/docs/adr/0009-session-cache-requested-model-identity.md +17 -0
- package/docs/adr/0010-mcp-server-thin-host.md +23 -0
- package/docs/agent-instructions.md +120 -0
- package/docs/design.md +16 -4
- package/docs/mcp.md +233 -0
- package/docs/tools/jev_ask.md +8 -5
- package/docs/tools/jev_ask_files.md +2 -1
- package/docs/tools/jev_check_diff.md +4 -1
- package/docs/tools/jev_find_files.md +2 -1
- package/docs/tools/jev_locate_in_file.md +5 -0
- package/docs/tools/jev_select_tests.md +4 -1
- package/package.json +19 -4
- package/rules/jev-ask.md +22 -1
- package/server.json +57 -0
- package/src/adapters/ask-files.ts +11 -3
- package/src/adapters/ask-proof.ts +69 -11
- package/src/adapters/canonical-path.ts +18 -0
- package/src/adapters/command.ts +102 -36
- package/src/adapters/docs.ts +33 -14
- package/src/adapters/evidence-context.ts +169 -0
- package/src/adapters/exec.ts +226 -0
- package/src/adapters/files.ts +146 -16
- package/src/adapters/find.ts +37 -7
- package/src/adapters/git-base.ts +7 -1
- package/src/adapters/git.ts +61 -8
- package/src/adapters/locate-file.ts +51 -9
- package/src/adapters/private-storage.ts +155 -0
- package/src/adapters/risk-callers.ts +7 -2
- package/src/adapters/shell.ts +113 -0
- package/src/adapters/test-inventory.ts +12 -4
- package/src/configuration.ts +55 -14
- package/src/constants.ts +37 -5
- package/src/core/ask-references.ts +262 -146
- package/src/core/asks.ts +79 -7
- package/src/core/command-output.ts +17 -1
- package/src/core/import-boundaries.ts +8 -3
- package/src/core/locate.ts +8 -5
- package/src/core/output.ts +34 -0
- package/src/core/result-report.ts +410 -0
- package/src/core/secret-path.ts +37 -0
- package/src/core/state.ts +8 -1
- package/src/core/units.ts +3 -2
- package/src/host.ts +11 -0
- package/src/index.ts +3 -0
- package/src/jev/client.ts +66 -16
- package/src/jev/types.ts +24 -3
- package/src/mcp/main.ts +135 -0
- package/src/mcp/protocol.ts +332 -0
- package/src/mcp/tools.ts +179 -0
- package/src/render.ts +109 -0
- package/src/report-schema.ts +1380 -0
- package/src/result-types.ts +234 -0
- package/src/result.ts +4 -1
- package/src/runtime.ts +6 -0
- package/src/session.ts +59 -0
- package/src/setup.ts +13 -5
- package/src/texts/ask-files.ts +4 -1
- package/src/texts/ask.ts +8 -1
- package/src/texts/check-diff.ts +7 -4
- package/src/texts/find.ts +8 -2
- package/src/texts/guide.ts +8 -16
- package/src/texts/instructions.ts +98 -0
- package/src/texts/locate.ts +8 -2
- package/src/texts/run-end.ts +2 -2
- package/src/texts/select-tests.ts +4 -1
- package/src/tools/ask-files.ts +311 -18
- package/src/tools/ask.ts +722 -95
- package/src/tools/check-diff.ts +337 -31
- package/src/tools/docs-check.ts +241 -38
- package/src/tools/find.ts +389 -29
- package/src/tools/locate.ts +387 -25
- package/src/tools/review-report.ts +308 -0
- package/src/tools/select-tests.ts +484 -23
- package/src/tools/spec-check.ts +194 -19
package/src/tools/ask.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { type Static, Type } from "@sinclair/typebox";
|
|
5
5
|
import { createAnalysisContext } from "../adapters/analysis-context.ts";
|
|
6
6
|
import { collectAskRepository, repositoryPath } from "../adapters/ask-proof.ts";
|
|
7
7
|
import { collectProofSyntax } from "../adapters/ask-syntax.ts";
|
|
8
|
+
import { canonicalPath } from "../adapters/canonical-path.ts";
|
|
8
9
|
import { captureCommand } from "../adapters/command.ts";
|
|
10
|
+
import {
|
|
11
|
+
type EvidenceContext,
|
|
12
|
+
resolveEvidenceContext,
|
|
13
|
+
withEvidenceContext,
|
|
14
|
+
} from "../adapters/evidence-context.ts";
|
|
9
15
|
import { collectFiles } from "../adapters/files.ts";
|
|
10
16
|
import { shareGitInventory } from "../adapters/git-inventory.ts";
|
|
11
17
|
import { hostUsage } from "../adapters/usage.ts";
|
|
@@ -20,6 +26,7 @@ import {
|
|
|
20
26
|
STATE_MAX_CHARS,
|
|
21
27
|
} from "../constants.ts";
|
|
22
28
|
import { buildAskClosure } from "../core/ask-closure.ts";
|
|
29
|
+
import type { ReferenceResolution } from "../core/ask-references.ts";
|
|
23
30
|
import {
|
|
24
31
|
blockedAskReadings,
|
|
25
32
|
resolveAskReferences,
|
|
@@ -31,26 +38,57 @@ import {
|
|
|
31
38
|
type ImportSource,
|
|
32
39
|
} from "../core/imports.ts";
|
|
33
40
|
import { checkIntegrity } from "../core/integrity.ts";
|
|
34
|
-
import type {
|
|
41
|
+
import type {
|
|
42
|
+
AnswerInput,
|
|
43
|
+
BudgetRefusal,
|
|
44
|
+
EnvelopeInput,
|
|
45
|
+
} from "../core/output.ts";
|
|
35
46
|
import { buildEnvelope } from "../core/output.ts";
|
|
47
|
+
import {
|
|
48
|
+
type Action,
|
|
49
|
+
answerItemFromInput,
|
|
50
|
+
buildResultReport,
|
|
51
|
+
type Cause,
|
|
52
|
+
type Context,
|
|
53
|
+
contextFromEvidence,
|
|
54
|
+
type Diagnostic,
|
|
55
|
+
type Item,
|
|
56
|
+
known,
|
|
57
|
+
notApplicable,
|
|
58
|
+
type ResultReportV1,
|
|
59
|
+
unknown,
|
|
60
|
+
} from "../core/result-report.ts";
|
|
36
61
|
import { assembleState } from "../core/state.ts";
|
|
37
62
|
import { discoverTests } from "../core/test-discovery.ts";
|
|
38
63
|
import { describeAsk } from "../describe.ts";
|
|
39
64
|
import type { GuideContext } from "../guide.ts";
|
|
40
65
|
import type { Judgment } from "../jev/types.ts";
|
|
41
|
-
import {
|
|
66
|
+
import { renderResultReport } from "../render.ts";
|
|
42
67
|
import { isRecord } from "../result.ts";
|
|
43
68
|
import type { ToolDependencies } from "../runtime.ts";
|
|
44
69
|
import { COMMAND_LIMIT_NOTICE } from "../texts/ask.ts";
|
|
45
70
|
import { NOT_CONFIGURED } from "../texts/configuration.ts";
|
|
71
|
+
import { ASK_GUIDELINE } from "../texts/instructions.ts";
|
|
46
72
|
import { asksParameter } from "./ask-schema.ts";
|
|
47
73
|
|
|
74
|
+
interface AskEvidenceFacts {
|
|
75
|
+
references?: ReferenceResolution;
|
|
76
|
+
omissions: {
|
|
77
|
+
reference: string;
|
|
78
|
+
reason: string;
|
|
79
|
+
origin?: string;
|
|
80
|
+
scope?: string;
|
|
81
|
+
required?: boolean;
|
|
82
|
+
}[];
|
|
83
|
+
blockedGroups: { ids: string[]; reason: string; cause?: Cause }[];
|
|
84
|
+
inventory: string[];
|
|
85
|
+
}
|
|
86
|
+
|
|
48
87
|
export const askParameters = Type.Object(
|
|
49
88
|
{
|
|
89
|
+
root: Type.Optional(Type.String()),
|
|
50
90
|
state: Type.Optional(Type.String({ maxLength: ASK_NOTE_MAX_CHARS })),
|
|
51
|
-
paths: Type.Optional(
|
|
52
|
-
Type.Array(Type.String({ minLength: 1 }), { maxItems: ASK_MAX_FILES }),
|
|
53
|
-
),
|
|
91
|
+
paths: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
54
92
|
base: Type.Optional(Type.String({ minLength: 1 })),
|
|
55
93
|
command: Type.Optional(Type.String({ minLength: 1 })),
|
|
56
94
|
timeout_s: Type.Optional(
|
|
@@ -88,9 +126,7 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
88
126
|
: {
|
|
89
127
|
promptSnippet:
|
|
90
128
|
"Typed answers about one situation built from a note, files and a command's output",
|
|
91
|
-
promptGuidelines: [
|
|
92
|
-
"jev_ask: before you conclude that a failure is a bug in the code, a wrong test or the environment, or that a plan matches the docs, pass the files to jev_ask and weigh its answer against your own reading",
|
|
93
|
-
],
|
|
129
|
+
promptGuidelines: [ASK_GUIDELINE],
|
|
94
130
|
}),
|
|
95
131
|
async execute(
|
|
96
132
|
_id: string,
|
|
@@ -100,7 +136,11 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
100
136
|
ctx: { cwd: string } & GuideContext,
|
|
101
137
|
): Promise<{
|
|
102
138
|
content: { type: "text"; text: string }[];
|
|
103
|
-
details: Judgment
|
|
139
|
+
details: Judgment & {
|
|
140
|
+
result: ResultReportV1;
|
|
141
|
+
evidenceContext: EvidenceContext;
|
|
142
|
+
evidenceFacts: AskEvidenceFacts;
|
|
143
|
+
};
|
|
104
144
|
usage?: {
|
|
105
145
|
input: number;
|
|
106
146
|
output: number;
|
|
@@ -117,7 +157,46 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
117
157
|
};
|
|
118
158
|
}> {
|
|
119
159
|
const client = dependencies.client;
|
|
160
|
+
const evidence = await resolveEvidenceContext(ctx.cwd, args.root, {
|
|
161
|
+
exec: execute,
|
|
162
|
+
signal,
|
|
163
|
+
origin: dependencies.evidenceOrigin,
|
|
164
|
+
});
|
|
165
|
+
const evidenceContext = evidence.context;
|
|
166
|
+
const cwd = evidence.ok ? evidence.cwd : evidenceContext.authority.path;
|
|
120
167
|
const started = performance.now();
|
|
168
|
+
const evidenceFacts: AskEvidenceFacts = {
|
|
169
|
+
omissions: [],
|
|
170
|
+
blockedGroups: [],
|
|
171
|
+
inventory: [],
|
|
172
|
+
};
|
|
173
|
+
evidenceContext.requestedBase = args.base;
|
|
174
|
+
const asks = compileAsks(args.asks, { surface: "ask" });
|
|
175
|
+
let commandContext: Context["command"] = args.command
|
|
176
|
+
? {
|
|
177
|
+
execution: "not_started",
|
|
178
|
+
cwd: evidence.ok
|
|
179
|
+
? known(cwd)
|
|
180
|
+
: unknown("effective root not established"),
|
|
181
|
+
exitCode: unknown("command not started"),
|
|
182
|
+
timedOut: unknown("command not started"),
|
|
183
|
+
}
|
|
184
|
+
: {
|
|
185
|
+
execution: "not_requested",
|
|
186
|
+
cwd: notApplicable("no command"),
|
|
187
|
+
exitCode: notApplicable("no command"),
|
|
188
|
+
timedOut: notApplicable("no command"),
|
|
189
|
+
};
|
|
190
|
+
let refusalCause: Cause = "invalid_arguments";
|
|
191
|
+
let inventoryCollected = false;
|
|
192
|
+
const auxiliary = {
|
|
193
|
+
controls: { fresh: 0, cache: 0, notJudged: 0, static: 0 },
|
|
194
|
+
passages: { fresh: 0, cache: 0, notJudged: 0 },
|
|
195
|
+
};
|
|
196
|
+
const attributionAnswers: Record<string, AnswerInput["reportControls"]> =
|
|
197
|
+
{};
|
|
198
|
+
const attributionMissing = new Set<string>();
|
|
199
|
+
const findResults: Judgment[] = [];
|
|
121
200
|
const exec = shareGitInventory(execute);
|
|
122
201
|
const finish = (
|
|
123
202
|
result: Judgment,
|
|
@@ -135,15 +214,324 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
135
214
|
},
|
|
136
215
|
});
|
|
137
216
|
runtime.session.record(envelope);
|
|
217
|
+
const diagnostics: Diagnostic[] = [];
|
|
218
|
+
const actions: Action[] = [];
|
|
219
|
+
const globalMissing = new Map<
|
|
220
|
+
string,
|
|
221
|
+
{ diagnosticIds: string[]; actionIds: string[] }
|
|
222
|
+
>();
|
|
223
|
+
const diagnose = (
|
|
224
|
+
cause: Cause,
|
|
225
|
+
fact: string,
|
|
226
|
+
scope: Diagnostic["scope"],
|
|
227
|
+
target: string | undefined,
|
|
228
|
+
origin: Diagnostic["origin"],
|
|
229
|
+
effect: Diagnostic["effect"] = "blocking",
|
|
230
|
+
next?: string,
|
|
231
|
+
) => {
|
|
232
|
+
const id = `diagnostic:${diagnostics.length}`;
|
|
233
|
+
const actionId = `action:${actions.length}`;
|
|
234
|
+
const code =
|
|
235
|
+
cause === "invalid_root" || cause === "invalid_base"
|
|
236
|
+
? "correct_context"
|
|
237
|
+
: cause === "forbidden_path"
|
|
238
|
+
? "correct_reference"
|
|
239
|
+
: cause === "missing_required" || cause === "empty_required"
|
|
240
|
+
? "provide_evidence"
|
|
241
|
+
: cause === "not_configured"
|
|
242
|
+
? "configure_client"
|
|
243
|
+
: "inspect_native";
|
|
244
|
+
actions.push({
|
|
245
|
+
id: actionId,
|
|
246
|
+
code,
|
|
247
|
+
target:
|
|
248
|
+
target === undefined
|
|
249
|
+
? unknown("target not established")
|
|
250
|
+
: known(target),
|
|
251
|
+
scope,
|
|
252
|
+
condition:
|
|
253
|
+
"Only if materially new evidence or corrected context is available",
|
|
254
|
+
instruction:
|
|
255
|
+
next ??
|
|
256
|
+
(code === "provide_evidence"
|
|
257
|
+
? `Provide the actually missing evidence${target ? `: ${target}` : ""}; do not repeat an already captured command.`
|
|
258
|
+
: "Inspect the supplied evidence natively; correct the named cause before any useful new Jev call."),
|
|
259
|
+
repeatUnchanged: false,
|
|
260
|
+
});
|
|
261
|
+
diagnostics.push({
|
|
262
|
+
id,
|
|
263
|
+
cause,
|
|
264
|
+
fact,
|
|
265
|
+
target:
|
|
266
|
+
target === undefined
|
|
267
|
+
? unknown("target not established")
|
|
268
|
+
: known(target),
|
|
269
|
+
origin,
|
|
270
|
+
scope,
|
|
271
|
+
effect,
|
|
272
|
+
material: true,
|
|
273
|
+
omittedMembers: [],
|
|
274
|
+
memberCount: known(0),
|
|
275
|
+
actionIds: [actionId],
|
|
276
|
+
});
|
|
277
|
+
return { diagnosticIds: [id], actionIds: [actionId] };
|
|
278
|
+
};
|
|
279
|
+
const items: Item[] = asks.ok
|
|
280
|
+
? asks.readings.map((reading, index) => {
|
|
281
|
+
const groupId = `group:${asks.groups.findIndex((group) => group.includes(reading.id))}`;
|
|
282
|
+
const target = asks.asks[reading.askIndex]?.about ?? "state";
|
|
283
|
+
const pieces: Item["evidence"] = [
|
|
284
|
+
{
|
|
285
|
+
target,
|
|
286
|
+
canonicalPath: unknown(
|
|
287
|
+
"selector does not establish a canonical file identity",
|
|
288
|
+
),
|
|
289
|
+
aliases: [],
|
|
290
|
+
side: target === "output" ? "output" : "state",
|
|
291
|
+
revision: notApplicable("non-file evidence"),
|
|
292
|
+
},
|
|
293
|
+
];
|
|
294
|
+
for (const [alias, path] of Object.entries(
|
|
295
|
+
evidenceFacts.references?.aliases ?? {},
|
|
296
|
+
)) {
|
|
297
|
+
const before =
|
|
298
|
+
evidenceFacts.references?.additions.some(
|
|
299
|
+
(addition) =>
|
|
300
|
+
addition.reference === alias &&
|
|
301
|
+
addition.side === "files_before",
|
|
302
|
+
) || target.includes("files_before");
|
|
303
|
+
pieces.push({
|
|
304
|
+
target: alias,
|
|
305
|
+
canonicalPath: evidenceContext.effectiveRoot
|
|
306
|
+
? known(resolve(evidenceContext.effectiveRoot.path, path))
|
|
307
|
+
: unknown("effective root unavailable"),
|
|
308
|
+
aliases: [alias],
|
|
309
|
+
side: before ? "before" : "current",
|
|
310
|
+
revision:
|
|
311
|
+
before && evidenceContext.resolvedBase
|
|
312
|
+
? known(evidenceContext.resolvedBase)
|
|
313
|
+
: unknown("working tree evidence"),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
const common = {
|
|
317
|
+
id: reading.id,
|
|
318
|
+
kind:
|
|
319
|
+
reading.kind === "verify"
|
|
320
|
+
? ("claim" as const)
|
|
321
|
+
: ("question" as const),
|
|
322
|
+
label: reading.label,
|
|
323
|
+
groupId,
|
|
324
|
+
evidence: pieces,
|
|
325
|
+
diagnosticIds: [] as string[],
|
|
326
|
+
actionIds: [] as string[],
|
|
327
|
+
};
|
|
328
|
+
const originalAnswer = input.answers?.[index];
|
|
329
|
+
const attributionControls = attributionAnswers[reading.id];
|
|
330
|
+
const answer =
|
|
331
|
+
originalAnswer && attributionMissing.has(reading.id)
|
|
332
|
+
? {
|
|
333
|
+
...originalAnswer,
|
|
334
|
+
unjudged: true,
|
|
335
|
+
reason: "Required attribution control is incomplete",
|
|
336
|
+
}
|
|
337
|
+
: originalAnswer && attributionControls
|
|
338
|
+
? {
|
|
339
|
+
...originalAnswer,
|
|
340
|
+
reportControls: [
|
|
341
|
+
...(originalAnswer.reportControls ?? []),
|
|
342
|
+
...attributionControls,
|
|
343
|
+
],
|
|
344
|
+
}
|
|
345
|
+
: originalAnswer;
|
|
346
|
+
if (
|
|
347
|
+
answer &&
|
|
348
|
+
!answer.unjudged &&
|
|
349
|
+
(answer.source || answer.answer?.source)
|
|
350
|
+
)
|
|
351
|
+
return answerItemFromInput(
|
|
352
|
+
common,
|
|
353
|
+
input.controls?.length
|
|
354
|
+
? {
|
|
355
|
+
...answer,
|
|
356
|
+
band: "unsure",
|
|
357
|
+
reason: input.controls
|
|
358
|
+
.map((control) => control.fact)
|
|
359
|
+
.join("; "),
|
|
360
|
+
}
|
|
361
|
+
: answer,
|
|
362
|
+
);
|
|
363
|
+
const blocked = evidenceFacts.blockedGroups.find((group) =>
|
|
364
|
+
group.ids.includes(reading.id),
|
|
365
|
+
);
|
|
366
|
+
const omission = evidenceFacts.omissions.find(
|
|
367
|
+
(item) =>
|
|
368
|
+
item.required &&
|
|
369
|
+
(item.origin === "note" || item.scope === reading.id),
|
|
370
|
+
);
|
|
371
|
+
const groupFailure = result.ok
|
|
372
|
+
? (
|
|
373
|
+
asks.groups.find((group) => group.includes(reading.id)) ??
|
|
374
|
+
[]
|
|
375
|
+
)
|
|
376
|
+
.map((id) => result.answers[id])
|
|
377
|
+
.find(
|
|
378
|
+
(answer) => answer?.type === "unjudged" && answer.cause,
|
|
379
|
+
)
|
|
380
|
+
: undefined;
|
|
381
|
+
const cause = blocked
|
|
382
|
+
? (blocked.cause ?? "missing_required")
|
|
383
|
+
: groupFailure?.type === "unjudged" && groupFailure.cause
|
|
384
|
+
? groupFailure.cause
|
|
385
|
+
: input.budget
|
|
386
|
+
? input.budget.kind === "session"
|
|
387
|
+
? "session_budget"
|
|
388
|
+
: "call_budget"
|
|
389
|
+
: (result.failureCause ??
|
|
390
|
+
(input.refusal ? refusalCause : "control_failure"));
|
|
391
|
+
const globalKey =
|
|
392
|
+
omission?.origin === "note" ? omission.reference : undefined;
|
|
393
|
+
const existingLinks = globalKey
|
|
394
|
+
? globalMissing.get(globalKey)
|
|
395
|
+
: undefined;
|
|
396
|
+
const links =
|
|
397
|
+
existingLinks ??
|
|
398
|
+
diagnose(
|
|
399
|
+
cause,
|
|
400
|
+
blocked?.reason ??
|
|
401
|
+
answer?.reason ??
|
|
402
|
+
(result.ok
|
|
403
|
+
? "Required judgment group incomplete or response provenance not established"
|
|
404
|
+
: result.error),
|
|
405
|
+
omission?.origin === "note"
|
|
406
|
+
? { kind: "call" }
|
|
407
|
+
: { kind: "group", groupIds: [groupId] },
|
|
408
|
+
omission?.reference,
|
|
409
|
+
omission?.origin === "note"
|
|
410
|
+
? "note"
|
|
411
|
+
: blocked
|
|
412
|
+
? "ask"
|
|
413
|
+
: input.budget
|
|
414
|
+
? "budget"
|
|
415
|
+
: "provider",
|
|
416
|
+
);
|
|
417
|
+
if (globalKey) globalMissing.set(globalKey, links);
|
|
418
|
+
return {
|
|
419
|
+
...common,
|
|
420
|
+
...links,
|
|
421
|
+
treatment: "not_judged",
|
|
422
|
+
source: "none",
|
|
423
|
+
};
|
|
424
|
+
})
|
|
425
|
+
: [];
|
|
426
|
+
if (!items.length)
|
|
427
|
+
diagnose(
|
|
428
|
+
input.refusal ? refusalCause : "collection_empty",
|
|
429
|
+
input.refusal ?? "No requested results collected",
|
|
430
|
+
{ kind: "call" },
|
|
431
|
+
undefined,
|
|
432
|
+
"input",
|
|
433
|
+
);
|
|
434
|
+
for (const control of input.controls ?? [])
|
|
435
|
+
diagnose(
|
|
436
|
+
"control_failure",
|
|
437
|
+
control.fact,
|
|
438
|
+
{ kind: "call" },
|
|
439
|
+
undefined,
|
|
440
|
+
"control",
|
|
441
|
+
"reservation",
|
|
442
|
+
control.next,
|
|
443
|
+
);
|
|
444
|
+
for (const limitation of input.limitations ?? [])
|
|
445
|
+
diagnose(
|
|
446
|
+
"evidence_limit",
|
|
447
|
+
limitation.fact,
|
|
448
|
+
{ kind: "call" },
|
|
449
|
+
limitation.path,
|
|
450
|
+
"closure",
|
|
451
|
+
"reservation",
|
|
452
|
+
limitation.next,
|
|
453
|
+
);
|
|
454
|
+
const report = buildResultReport({
|
|
455
|
+
tool: "jev_ask",
|
|
456
|
+
context: contextFromEvidence(evidenceContext, {
|
|
457
|
+
command: commandContext,
|
|
458
|
+
inventories: inventoryCollected
|
|
459
|
+
? [
|
|
460
|
+
{
|
|
461
|
+
id: "repository",
|
|
462
|
+
kind: "repository",
|
|
463
|
+
rules: ["Git tracked and admitted repository evidence"],
|
|
464
|
+
restrictions: args.paths ?? [],
|
|
465
|
+
discovered: known(evidenceFacts.inventory.length),
|
|
466
|
+
considered: known(evidenceFacts.inventory.length),
|
|
467
|
+
scopeRestricted: false,
|
|
468
|
+
criteria: [],
|
|
469
|
+
},
|
|
470
|
+
]
|
|
471
|
+
: [],
|
|
472
|
+
}),
|
|
473
|
+
items,
|
|
474
|
+
diagnostics,
|
|
475
|
+
actions,
|
|
476
|
+
metrics: {
|
|
477
|
+
calls: result.calls ?? 0,
|
|
478
|
+
questions: result.questions ?? 0,
|
|
479
|
+
cacheHits: result.cacheHits ?? 0,
|
|
480
|
+
cacheRequests: result.cacheRequests ?? 0,
|
|
481
|
+
costUsd: result.usage?.costUsd,
|
|
482
|
+
elapsedMs: performance.now() - started,
|
|
483
|
+
},
|
|
484
|
+
auxiliary,
|
|
485
|
+
total: known(items.length),
|
|
486
|
+
refused:
|
|
487
|
+
!!input.refusal &&
|
|
488
|
+
refusalCause !== "not_configured" &&
|
|
489
|
+
!result.failureCause,
|
|
490
|
+
});
|
|
138
491
|
runtime.guide.deliver(ctx);
|
|
139
492
|
return {
|
|
140
|
-
content: [
|
|
141
|
-
|
|
493
|
+
content: [
|
|
494
|
+
{
|
|
495
|
+
type: "text" as const,
|
|
496
|
+
text: renderResultReport(report, { details: envelope }),
|
|
497
|
+
},
|
|
498
|
+
],
|
|
499
|
+
details: {
|
|
500
|
+
...result,
|
|
501
|
+
result: report,
|
|
502
|
+
evidenceContext,
|
|
503
|
+
evidenceFacts,
|
|
504
|
+
},
|
|
142
505
|
};
|
|
143
506
|
};
|
|
144
|
-
const textResult = (text: string) =>
|
|
145
|
-
|
|
146
|
-
|
|
507
|
+
const textResult = (text: string, cause: Cause = "invalid_arguments") => {
|
|
508
|
+
refusalCause = cause;
|
|
509
|
+
const technical = findResults.reduce(
|
|
510
|
+
(totals, judgment) => ({
|
|
511
|
+
calls: totals.calls + (judgment.calls ?? 0),
|
|
512
|
+
questions: totals.questions + (judgment.questions ?? 0),
|
|
513
|
+
cacheHits: totals.cacheHits + (judgment.cacheHits ?? 0),
|
|
514
|
+
cacheRequests: totals.cacheRequests + (judgment.cacheRequests ?? 0),
|
|
515
|
+
}),
|
|
516
|
+
{ calls: 0, questions: 0, cacheHits: 0, cacheRequests: 0 },
|
|
517
|
+
);
|
|
518
|
+
return finish(
|
|
519
|
+
{ ok: false, error: text, ...technical },
|
|
520
|
+
{ refusal: text },
|
|
521
|
+
);
|
|
522
|
+
};
|
|
523
|
+
if (!evidence.ok)
|
|
524
|
+
return textResult(evidence.error, evidence.cause ?? "invalid_root");
|
|
525
|
+
if (!asks.ok) return textResult(asks.error);
|
|
526
|
+
const preliminary = resolveAskReferences(asks, args.state, {}, []);
|
|
527
|
+
if (preliminary.invalid.length) {
|
|
528
|
+
evidenceFacts.references = preliminary;
|
|
529
|
+
return textResult(
|
|
530
|
+
preliminary.invalid.map((item) => item.reason).join("; "),
|
|
531
|
+
"forbidden_path",
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
if (!client) return textResult(NOT_CONFIGURED, "not_configured");
|
|
147
535
|
if (args.command && !allowCommand)
|
|
148
536
|
return textResult("command disabled by JEV_TOOLS_ALLOW_COMMAND=0");
|
|
149
537
|
if (
|
|
@@ -157,69 +545,115 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
157
545
|
);
|
|
158
546
|
if (args.state === undefined && !args.paths?.length && !args.command)
|
|
159
547
|
return textResult("Provide state, command or at least one path.");
|
|
160
|
-
const
|
|
161
|
-
if (!asks.ok) return textResult(asks.error);
|
|
162
|
-
const files = await collectFiles(ctx.cwd, args.paths ?? [], signal, {
|
|
548
|
+
const files = await collectFiles(cwd, args.paths ?? [], signal, {
|
|
163
549
|
exec,
|
|
164
550
|
allowEmpty: true,
|
|
165
551
|
skipInvalidUtf8: true,
|
|
166
552
|
});
|
|
167
|
-
if (!files.ok)
|
|
553
|
+
if (!files.ok)
|
|
554
|
+
return textResult(files.error, files.cause ?? "file_unavailable");
|
|
168
555
|
if (
|
|
169
556
|
!Object.keys(files.files).length &&
|
|
170
557
|
args.state === undefined &&
|
|
171
558
|
!args.command &&
|
|
172
559
|
files.skipped.length
|
|
173
560
|
)
|
|
174
|
-
return textResult(
|
|
561
|
+
return textResult(
|
|
562
|
+
`No readable evidence: ${files.skipped.join("; ")}`,
|
|
563
|
+
"collection_empty",
|
|
564
|
+
);
|
|
175
565
|
const proofLimitations: Array<
|
|
176
566
|
NonNullable<EnvelopeInput["limitations"]>[number]
|
|
177
567
|
> = files.skipped.map((fact) => ({
|
|
178
568
|
fact,
|
|
179
569
|
next: "Provide this file as UTF-8 text if the judgment needs it.",
|
|
180
570
|
}));
|
|
181
|
-
let repository = await collectAskRepository(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (!repository.ok && args.base)
|
|
571
|
+
let repository = await collectAskRepository(exec, cwd, args.base, signal);
|
|
572
|
+
inventoryCollected = repository.ok;
|
|
573
|
+
if (repository.ok) {
|
|
574
|
+
evidenceFacts.inventory = [...repository.known];
|
|
575
|
+
evidenceContext.resolvedBase = repository.baseSha;
|
|
576
|
+
}
|
|
577
|
+
if (!repository.ok && args.base)
|
|
578
|
+
return textResult(repository.error, "invalid_base");
|
|
188
579
|
if (!repository.ok)
|
|
189
580
|
proofLimitations.push({
|
|
190
581
|
fact: `closure unavailable: ${repository.error}`,
|
|
191
582
|
next: "pass dependencies explicitly in paths",
|
|
192
583
|
});
|
|
584
|
+
if (args.command)
|
|
585
|
+
commandContext = {
|
|
586
|
+
...commandContext,
|
|
587
|
+
execution: "started",
|
|
588
|
+
cwd: known(repository.ok ? repository.root : cwd),
|
|
589
|
+
};
|
|
193
590
|
const command = args.command
|
|
194
591
|
? await captureCommand(
|
|
195
592
|
exec,
|
|
196
|
-
repository.ok ? repository.root :
|
|
593
|
+
repository.ok ? repository.root : cwd,
|
|
197
594
|
args.command,
|
|
198
595
|
args.timeout_s,
|
|
199
596
|
signal,
|
|
597
|
+
dependencies.apiKey ?? process.env.JEV_TOOLS_API_KEY,
|
|
200
598
|
)
|
|
201
599
|
: undefined;
|
|
202
|
-
if (command
|
|
600
|
+
if (command?.ok)
|
|
601
|
+
commandContext = {
|
|
602
|
+
execution: "finished",
|
|
603
|
+
cwd: known(repository.ok ? repository.root : cwd),
|
|
604
|
+
exitCode: known(command.output.exit_code),
|
|
605
|
+
timedOut: known(command.output.timed_out),
|
|
606
|
+
};
|
|
607
|
+
if (command && !command.ok) {
|
|
608
|
+
commandContext = {
|
|
609
|
+
// Reported execution states are not_requested, not_started, started
|
|
610
|
+
// and finished. A spawn whose completion was not observed (adapter
|
|
611
|
+
// "unknown") did start, so it reports "started" with unknown exit
|
|
612
|
+
// code and timeout.
|
|
613
|
+
execution:
|
|
614
|
+
command.commandExecution === "unknown"
|
|
615
|
+
? "started"
|
|
616
|
+
: command.commandExecution,
|
|
617
|
+
cwd: known(repository.ok ? repository.root : cwd),
|
|
618
|
+
exitCode:
|
|
619
|
+
command.commandExitCode !== undefined
|
|
620
|
+
? known(command.commandExitCode)
|
|
621
|
+
: unknown("command completion not observed"),
|
|
622
|
+
timedOut:
|
|
623
|
+
command.commandTimedOut !== undefined
|
|
624
|
+
? known(command.commandTimedOut)
|
|
625
|
+
: unknown("command completion not observed"),
|
|
626
|
+
};
|
|
627
|
+
return textResult(command.error, command.cause ?? "file_unavailable");
|
|
628
|
+
}
|
|
203
629
|
if (command?.ok) {
|
|
204
|
-
repository = await collectAskRepository(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
args.base,
|
|
208
|
-
signal,
|
|
209
|
-
);
|
|
210
|
-
if (!repository.ok && args.base) return textResult(repository.error);
|
|
630
|
+
repository = await collectAskRepository(exec, cwd, args.base, signal);
|
|
631
|
+
if (!repository.ok && args.base)
|
|
632
|
+
return textResult(repository.error, "invalid_base");
|
|
211
633
|
}
|
|
212
634
|
const initialState = assembleState(args.state, files.files);
|
|
213
635
|
if (!initialState.ok) return textResult(initialState.error);
|
|
214
636
|
if (command?.ok && Object.hasOwn(initialState.state, "output"))
|
|
215
637
|
return textResult(
|
|
216
638
|
"state contains reserved key output when command is supplied; rename it in your note",
|
|
639
|
+
"reserved_key",
|
|
217
640
|
);
|
|
218
641
|
let outputNotIdentifiable = false;
|
|
219
642
|
if (command?.ok && repository.ok) {
|
|
220
643
|
for (const target of command.targets) {
|
|
221
644
|
if (target.path.startsWith(`${repository.root}/`))
|
|
222
645
|
target.path = target.path.slice(repository.root.length + 1);
|
|
646
|
+
else if (isAbsolute(target.path)) {
|
|
647
|
+
// Windows reports C:/... or short 8.3 forms; compare canonically.
|
|
648
|
+
const local = relative(
|
|
649
|
+
repository.root,
|
|
650
|
+
await canonicalPath(target.path),
|
|
651
|
+
)
|
|
652
|
+
.split("\\")
|
|
653
|
+
.join("/");
|
|
654
|
+
if (local && !local.startsWith("../") && !isAbsolute(local))
|
|
655
|
+
target.path = local;
|
|
656
|
+
}
|
|
223
657
|
let matches = [...repository.known].filter(
|
|
224
658
|
(path) => path === target.path || path.endsWith(`/${target.path}`),
|
|
225
659
|
);
|
|
@@ -252,7 +686,7 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
252
686
|
}
|
|
253
687
|
const path = matches[0];
|
|
254
688
|
if (!path) continue;
|
|
255
|
-
const local = repositoryPath(
|
|
689
|
+
const local = repositoryPath(cwd, repository.root, path);
|
|
256
690
|
if (!Object.hasOwn(files.files, local)) {
|
|
257
691
|
const added = await collectFiles(repository.root, [path], signal, {
|
|
258
692
|
exec,
|
|
@@ -306,35 +740,75 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
306
740
|
const references = resolveAskReferences(
|
|
307
741
|
asks,
|
|
308
742
|
args.state,
|
|
309
|
-
|
|
743
|
+
repository.ok
|
|
744
|
+
? {
|
|
745
|
+
files: Object.fromEntries(
|
|
746
|
+
Object.entries(files.files).map(([path, text]) => [
|
|
747
|
+
repositoryPath(repository.root, cwd, path),
|
|
748
|
+
text,
|
|
749
|
+
]),
|
|
750
|
+
),
|
|
751
|
+
beforeInventory: args.base ? [...repository.beforeKnown] : [],
|
|
752
|
+
}
|
|
753
|
+
: files.files,
|
|
310
754
|
repository?.ok ? [...repository.known] : [],
|
|
311
755
|
);
|
|
756
|
+
evidenceFacts.references = references;
|
|
757
|
+
evidenceFacts.inventory = repository.ok ? [...repository.known] : [];
|
|
758
|
+
if (references.invalid.length)
|
|
759
|
+
return textResult(
|
|
760
|
+
references.invalid.map((item) => item.reason).join("; "),
|
|
761
|
+
"forbidden_path",
|
|
762
|
+
);
|
|
312
763
|
const unresolved = [...references.unresolved];
|
|
764
|
+
const historical: Record<string, string | null> = {};
|
|
765
|
+
if (repository.ok && args.base) {
|
|
766
|
+
for (const reference of references.additions.filter(
|
|
767
|
+
(reference) =>
|
|
768
|
+
reference.side === "files_before" && reference.required,
|
|
769
|
+
)) {
|
|
770
|
+
const before = await repository.readBefore(reference.path);
|
|
771
|
+
if (before.ok) historical[reference.path] = before.text;
|
|
772
|
+
else {
|
|
773
|
+
if (before.cause === "forbidden_path")
|
|
774
|
+
return textResult(before.error, before.cause);
|
|
775
|
+
unresolved.push({
|
|
776
|
+
...reference,
|
|
777
|
+
reason: before.error,
|
|
778
|
+
...(before.cause ? { cause: before.cause } : {}),
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
313
783
|
if (repository?.ok) {
|
|
314
784
|
const root = repository.root;
|
|
315
785
|
const additions = await Promise.all(
|
|
316
|
-
references.additions
|
|
317
|
-
addition
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
786
|
+
references.additions
|
|
787
|
+
.filter((addition) => addition.side !== "files_before")
|
|
788
|
+
.map(async (addition) => ({
|
|
789
|
+
addition,
|
|
790
|
+
added: await collectFiles(root, [addition.path], signal, {
|
|
791
|
+
allowEmpty: true,
|
|
792
|
+
exec,
|
|
793
|
+
inventory: repository.known,
|
|
794
|
+
}),
|
|
795
|
+
})),
|
|
324
796
|
);
|
|
325
797
|
for (const { addition, added } of additions) {
|
|
326
798
|
if (added.ok) {
|
|
327
|
-
|
|
799
|
+
const local = repositoryPath(cwd, root, addition.path);
|
|
800
|
+
files.files[local] = added.files[addition.path] ?? "";
|
|
328
801
|
files.identities.push(
|
|
329
802
|
...added.identities.map((identity) => ({
|
|
330
803
|
...identity,
|
|
331
|
-
requestedPath:
|
|
804
|
+
requestedPath: local,
|
|
332
805
|
})),
|
|
333
806
|
);
|
|
334
807
|
} else
|
|
335
808
|
unresolved.push({
|
|
336
|
-
|
|
809
|
+
...addition,
|
|
337
810
|
reason: added.error,
|
|
811
|
+
...(added.cause ? { cause: added.cause } : {}),
|
|
338
812
|
});
|
|
339
813
|
}
|
|
340
814
|
}
|
|
@@ -343,44 +817,58 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
343
817
|
if (command?.ok && Object.hasOwn(state.state, "output"))
|
|
344
818
|
return textResult(
|
|
345
819
|
"state contains reserved key output when command is supplied; rename it in your note",
|
|
820
|
+
"reserved_key",
|
|
346
821
|
);
|
|
347
822
|
if (command?.ok) state.state.output = { ...command.output };
|
|
348
823
|
if (repository?.ok) {
|
|
824
|
+
if (evidenceContext.effectiveRoot)
|
|
825
|
+
evidenceContext.effectiveRoot.path = repository.root;
|
|
826
|
+
evidenceContext.requestedBase = args.base;
|
|
827
|
+
evidenceContext.resolvedBase = repository.baseSha;
|
|
349
828
|
const canonical = Object.fromEntries(
|
|
350
829
|
Object.entries(files.files).map(([path, text]) => [
|
|
351
|
-
repositoryPath(repository.root,
|
|
830
|
+
repositoryPath(repository.root, cwd, path),
|
|
352
831
|
text,
|
|
353
832
|
]),
|
|
354
833
|
);
|
|
355
|
-
|
|
356
|
-
|
|
834
|
+
state.state.files = canonical;
|
|
835
|
+
state.state.evidence = {
|
|
836
|
+
context: evidenceContext,
|
|
837
|
+
aliases: Object.fromEntries(
|
|
838
|
+
Object.entries(references.aliases)
|
|
839
|
+
.map(([alias, path]): [string, string] => [
|
|
840
|
+
alias,
|
|
841
|
+
repository.known.has(path) || repository.beforeKnown.has(path)
|
|
842
|
+
? path
|
|
843
|
+
: repositoryPath(repository.root, cwd, path),
|
|
844
|
+
])
|
|
845
|
+
.sort(([a], [b]) => a.localeCompare(b)),
|
|
846
|
+
),
|
|
847
|
+
};
|
|
848
|
+
// Canonical repository paths are the sole serialized content keys.
|
|
357
849
|
const sources: ImportSource[] = Object.entries(canonical).map(
|
|
358
850
|
([path, text]) => ({ path, text }),
|
|
359
851
|
);
|
|
360
|
-
const before: Record<string, string | null> = {};
|
|
852
|
+
const before: Record<string, string | null> = { ...historical };
|
|
361
853
|
if (args.base) {
|
|
362
854
|
for (const path of Object.keys(canonical)) {
|
|
363
855
|
const old = await repository.readBefore(path);
|
|
364
|
-
if (!old.ok)
|
|
856
|
+
if (!old.ok)
|
|
857
|
+
return textResult(old.error, old.cause ?? "file_unavailable");
|
|
365
858
|
before[path] = old.text;
|
|
366
859
|
}
|
|
367
860
|
state.state.base_sha = repository.baseSha ?? "";
|
|
368
|
-
state.state.files_before =
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
(addition) => addition.reference === path,
|
|
373
|
-
)?.path ?? repositoryPath(repository.root, ctx.cwd, path);
|
|
374
|
-
return [path, before[canonicalPath] ?? null];
|
|
375
|
-
}),
|
|
376
|
-
);
|
|
377
|
-
const stateSize = JSON.stringify(state.state).length;
|
|
861
|
+
state.state.files_before = before;
|
|
862
|
+
const stateSize = JSON.stringify(
|
|
863
|
+
command?.ok ? { ...state.state, output: undefined } : state.state,
|
|
864
|
+
).length;
|
|
378
865
|
if (stateSize > STATE_MAX_CHARS) {
|
|
379
866
|
const partSizes = Object.entries(state.state)
|
|
380
867
|
.map(([part, value]) => `${part} ${JSON.stringify(value).length}`)
|
|
381
868
|
.join(", ");
|
|
382
869
|
return textResult(
|
|
383
870
|
`serialized state including files_before exceeds ${STATE_MAX_CHARS} chars (${stateSize}); serialized parts: ${partSizes}; split the situation into smaller states.`,
|
|
871
|
+
"evidence_too_large",
|
|
384
872
|
);
|
|
385
873
|
}
|
|
386
874
|
}
|
|
@@ -480,14 +968,17 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
480
968
|
sources,
|
|
481
969
|
...(args.base ? { before } : {}),
|
|
482
970
|
};
|
|
971
|
+
const closureState = { ...state.state };
|
|
972
|
+
if (command?.ok) delete closureState.output;
|
|
483
973
|
const closure = buildAskClosure({
|
|
484
974
|
graph: { edges, limits },
|
|
485
975
|
syntax,
|
|
486
976
|
paths: Object.keys(canonical),
|
|
487
977
|
intentions: JSON.stringify(asks.asks),
|
|
488
|
-
state:
|
|
978
|
+
state: closureState,
|
|
489
979
|
});
|
|
490
980
|
state.state = closure.state;
|
|
981
|
+
if (command?.ok) state.state.output = { ...command.output };
|
|
491
982
|
proofLimitations.push(
|
|
492
983
|
...closure.limitations.filter(
|
|
493
984
|
(limit) => !limit.fact.startsWith("Import closure only,"),
|
|
@@ -502,6 +993,22 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
502
993
|
fact: "closure: +0; imports only, depth 1; repository inventory unavailable",
|
|
503
994
|
next: "pass dependencies explicitly in paths",
|
|
504
995
|
});
|
|
996
|
+
if (!repository.ok)
|
|
997
|
+
state.state.evidence = {
|
|
998
|
+
context: evidenceContext,
|
|
999
|
+
aliases: references.aliases,
|
|
1000
|
+
};
|
|
1001
|
+
// Command output has its own passage-selection budget below. Only immutable
|
|
1002
|
+
// file/note evidence can refuse here, before selection has had a chance.
|
|
1003
|
+
const serializedSize = JSON.stringify(
|
|
1004
|
+
command?.ok ? { ...state.state, output: undefined } : state.state,
|
|
1005
|
+
).length;
|
|
1006
|
+
if (serializedSize > STATE_MAX_CHARS)
|
|
1007
|
+
return textResult(
|
|
1008
|
+
`serialized state exceeds ${STATE_MAX_CHARS} chars (${serializedSize}); files and versions are never truncated`,
|
|
1009
|
+
"evidence_too_large",
|
|
1010
|
+
);
|
|
1011
|
+
evidenceFacts.omissions = unresolved;
|
|
505
1012
|
const blocked = blockedAskReadings(
|
|
506
1013
|
asks,
|
|
507
1014
|
state.state,
|
|
@@ -510,13 +1017,16 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
510
1017
|
);
|
|
511
1018
|
const inserted = isRecord(state.state.files) ? state.state.files : {};
|
|
512
1019
|
const identities = files.identities.map((file) => {
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
1020
|
+
const insertedPath = repository.ok
|
|
1021
|
+
? repositoryPath(repository.root, cwd, file.requestedPath)
|
|
1022
|
+
: file.requestedPath;
|
|
1023
|
+
const content = inserted[insertedPath];
|
|
517
1024
|
return {
|
|
518
1025
|
...file,
|
|
519
|
-
insertedPath:
|
|
1026
|
+
insertedPath: resolve(
|
|
1027
|
+
repository.ok ? repository.root : cwd,
|
|
1028
|
+
insertedPath,
|
|
1029
|
+
),
|
|
520
1030
|
content: typeof content === "string" ? content : "",
|
|
521
1031
|
insertedSha256: createHash("sha256")
|
|
522
1032
|
.update(typeof content === "string" ? content : "")
|
|
@@ -528,6 +1038,24 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
528
1038
|
.filter((group) => group.some((id) => blocked.has(id)))
|
|
529
1039
|
.flat(),
|
|
530
1040
|
);
|
|
1041
|
+
evidenceFacts.blockedGroups = asks.groups
|
|
1042
|
+
.filter((group) => group.some((id) => blocked.has(id)))
|
|
1043
|
+
.map((ids) => {
|
|
1044
|
+
const reason =
|
|
1045
|
+
ids
|
|
1046
|
+
.map((id) => blocked.get(id))
|
|
1047
|
+
.find((reason) => reason !== undefined) ??
|
|
1048
|
+
"Required evidence unavailable";
|
|
1049
|
+
// A secret refusal stays named as such instead of missing_required.
|
|
1050
|
+
const secret = unresolved.some(
|
|
1051
|
+
(item) => item.reason === reason && item.cause === "secret_pattern",
|
|
1052
|
+
);
|
|
1053
|
+
return {
|
|
1054
|
+
ids,
|
|
1055
|
+
reason,
|
|
1056
|
+
...(secret ? { cause: "secret_pattern" as const } : {}),
|
|
1057
|
+
};
|
|
1058
|
+
});
|
|
531
1059
|
const questions = Object.fromEntries(
|
|
532
1060
|
Object.entries(asks.questions).filter(
|
|
533
1061
|
([id]) => !blockedMembers.has(id),
|
|
@@ -540,10 +1068,14 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
540
1068
|
);
|
|
541
1069
|
let sent = 0;
|
|
542
1070
|
let refusal: BudgetRefusal | undefined;
|
|
543
|
-
const findResults: Judgment[] = [];
|
|
544
1071
|
const options = {
|
|
545
1072
|
signal,
|
|
546
1073
|
cache: !args.command,
|
|
1074
|
+
...runtime.session.requestGate(),
|
|
1075
|
+
admissionCause: () =>
|
|
1076
|
+
refusal?.kind === "session"
|
|
1077
|
+
? ("session_budget" as const)
|
|
1078
|
+
: ("call_budget" as const),
|
|
547
1079
|
beforeRequest: (questionCount: number) => {
|
|
548
1080
|
if (args.max_calls !== undefined && sent >= args.max_calls) {
|
|
549
1081
|
refusal = {
|
|
@@ -578,11 +1110,25 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
578
1110
|
)
|
|
579
1111
|
return textResult(
|
|
580
1112
|
`output requires more than ${OUTPUT_FIND_MAX_CALLS} find calls: ${command.originalBytes} bytes before, ${command.compressedChars} chars after compression; narrow command (remove verbosity or filter)`,
|
|
1113
|
+
"evidence_too_large",
|
|
1114
|
+
);
|
|
1115
|
+
const chunkStates = chunks.map((chunk) =>
|
|
1116
|
+
withEvidenceContext({ output: chunk.text }, evidenceContext),
|
|
1117
|
+
);
|
|
1118
|
+
if (
|
|
1119
|
+
chunkStates.some(
|
|
1120
|
+
(chunkState) =>
|
|
1121
|
+
JSON.stringify(chunkState).length > STATE_MAX_CHARS,
|
|
1122
|
+
)
|
|
1123
|
+
)
|
|
1124
|
+
return textResult(
|
|
1125
|
+
`serialized output chunk including evidence context exceeds ${STATE_MAX_CHARS} chars; narrow command`,
|
|
1126
|
+
"evidence_too_large",
|
|
581
1127
|
);
|
|
582
1128
|
const found = await Promise.all(
|
|
583
|
-
|
|
1129
|
+
chunkStates.map(async (chunkState) => {
|
|
584
1130
|
const judgment = await client.judge(
|
|
585
|
-
|
|
1131
|
+
chunkState,
|
|
586
1132
|
{
|
|
587
1133
|
find: {
|
|
588
1134
|
type: "bool",
|
|
@@ -616,6 +1162,7 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
616
1162
|
if (budget < 100)
|
|
617
1163
|
return textResult(
|
|
618
1164
|
"No space for command output; reduce paths or state.",
|
|
1165
|
+
"evidence_too_large",
|
|
619
1166
|
);
|
|
620
1167
|
const stderrSize = JSON.stringify(output.stderr).length;
|
|
621
1168
|
const stdoutSize = JSON.stringify(output.stdout).length;
|
|
@@ -678,6 +1225,7 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
678
1225
|
if (JSON.stringify(state.state).length > STATE_MAX_CHARS)
|
|
679
1226
|
return textResult(
|
|
680
1227
|
"serialized command output exceeds state budget; narrow command or reduce paths",
|
|
1228
|
+
"evidence_too_large",
|
|
681
1229
|
);
|
|
682
1230
|
if (outputNotIdentifiable)
|
|
683
1231
|
integrity.controls.push({
|
|
@@ -730,13 +1278,14 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
730
1278
|
cacheHits: (first.cacheHits ?? 0) + (reversed.cacheHits ?? 0),
|
|
731
1279
|
cacheRequests:
|
|
732
1280
|
(first.cacheRequests ?? 0) + (reversed.cacheRequests ?? 0),
|
|
733
|
-
usage:
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1281
|
+
usage:
|
|
1282
|
+
first.usage && reversed.usage
|
|
1283
|
+
? {
|
|
1284
|
+
inputTokens:
|
|
1285
|
+
first.usage.inputTokens + reversed.usage.inputTokens,
|
|
1286
|
+
costUsd: first.usage.costUsd + reversed.usage.costUsd,
|
|
1287
|
+
}
|
|
1288
|
+
: undefined,
|
|
740
1289
|
};
|
|
741
1290
|
}
|
|
742
1291
|
}
|
|
@@ -745,12 +1294,17 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
745
1294
|
...result,
|
|
746
1295
|
calls: (result.calls ?? 0) + (found.calls ?? 0),
|
|
747
1296
|
questions: (result.questions ?? 0) + (found.questions ?? 0),
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1297
|
+
cacheHits: (result.cacheHits ?? 0) + (found.cacheHits ?? 0),
|
|
1298
|
+
cacheRequests:
|
|
1299
|
+
(result.cacheRequests ?? 0) + (found.cacheRequests ?? 0),
|
|
1300
|
+
usage:
|
|
1301
|
+
result.usage && found.usage
|
|
1302
|
+
? {
|
|
1303
|
+
inputTokens:
|
|
1304
|
+
result.usage.inputTokens + found.usage.inputTokens,
|
|
1305
|
+
costUsd: result.usage.costUsd + found.usage.costUsd,
|
|
1306
|
+
}
|
|
1307
|
+
: undefined,
|
|
754
1308
|
};
|
|
755
1309
|
const attribution: Array<
|
|
756
1310
|
NonNullable<EnvelopeInput["limitations"]>[number]
|
|
@@ -775,6 +1329,8 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
775
1329
|
fact: `attribution ${reading.label}: no designated candidate`,
|
|
776
1330
|
next: "provide decisive candidate evidence before acting",
|
|
777
1331
|
});
|
|
1332
|
+
attributionMissing.add(reading.id);
|
|
1333
|
+
auxiliary.controls.notJudged++;
|
|
778
1334
|
continue;
|
|
779
1335
|
}
|
|
780
1336
|
if (!Object.hasOwn(inserted, selected)) {
|
|
@@ -782,19 +1338,21 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
782
1338
|
fact: `attribution ${reading.label}: designated piece ${selected} is absent`,
|
|
783
1339
|
next: `add ${selected} to paths before acting`,
|
|
784
1340
|
});
|
|
1341
|
+
attributionMissing.add(reading.id);
|
|
1342
|
+
auxiliary.controls.notJudged++;
|
|
785
1343
|
continue;
|
|
786
1344
|
}
|
|
787
1345
|
const selectedPath = repository?.ok
|
|
788
1346
|
? (references.additions.find(
|
|
789
1347
|
(addition) => addition.reference === selected,
|
|
790
|
-
)?.path ?? repositoryPath(repository.root,
|
|
1348
|
+
)?.path ?? repositoryPath(repository.root, cwd, selected))
|
|
791
1349
|
: selected;
|
|
792
1350
|
const retained = (path: string) => {
|
|
793
1351
|
if (!repository?.ok) return path !== selected;
|
|
794
1352
|
return (
|
|
795
1353
|
(references.additions.find(
|
|
796
1354
|
(addition) => addition.reference === path,
|
|
797
|
-
)?.path ?? repositoryPath(repository.root,
|
|
1355
|
+
)?.path ?? repositoryPath(repository.root, cwd, path)) !==
|
|
798
1356
|
selectedPath
|
|
799
1357
|
);
|
|
800
1358
|
};
|
|
@@ -830,6 +1388,40 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
830
1388
|
options,
|
|
831
1389
|
);
|
|
832
1390
|
const answer = control.ok ? control.answers[reading.id] : undefined;
|
|
1391
|
+
if (answer && answer.type !== "unjudged" && answer.source)
|
|
1392
|
+
attributionAnswers[reading.id] = [
|
|
1393
|
+
{
|
|
1394
|
+
id: `${reading.id}:attribution`,
|
|
1395
|
+
kind: "attribution",
|
|
1396
|
+
source: answer.source,
|
|
1397
|
+
outcome:
|
|
1398
|
+
answer.type === "choice"
|
|
1399
|
+
? answer.choice === selected
|
|
1400
|
+
? "does not rest on it"
|
|
1401
|
+
: "attributed"
|
|
1402
|
+
: "invalid attribution response",
|
|
1403
|
+
rawValues:
|
|
1404
|
+
answer.type === "bool"
|
|
1405
|
+
? [
|
|
1406
|
+
{
|
|
1407
|
+
label: "attribution",
|
|
1408
|
+
value: answer.p >= 0.5,
|
|
1409
|
+
probability: known(answer.p),
|
|
1410
|
+
},
|
|
1411
|
+
]
|
|
1412
|
+
: Object.entries(answer.probabilities).map(
|
|
1413
|
+
([label, value]) => ({
|
|
1414
|
+
label,
|
|
1415
|
+
value: label,
|
|
1416
|
+
probability: known(value),
|
|
1417
|
+
}),
|
|
1418
|
+
),
|
|
1419
|
+
},
|
|
1420
|
+
];
|
|
1421
|
+
if (answer?.type !== "choice") attributionMissing.add(reading.id);
|
|
1422
|
+
if (answer && answer.type !== "unjudged" && answer.source)
|
|
1423
|
+
auxiliary.controls[answer.source]++;
|
|
1424
|
+
else auxiliary.controls.notJudged++;
|
|
833
1425
|
attribution.push(
|
|
834
1426
|
answer?.type === "choice"
|
|
835
1427
|
? {
|
|
@@ -848,13 +1440,14 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
848
1440
|
cacheHits: (result.cacheHits ?? 0) + (control.cacheHits ?? 0),
|
|
849
1441
|
cacheRequests:
|
|
850
1442
|
(result.cacheRequests ?? 0) + (control.cacheRequests ?? 0),
|
|
851
|
-
usage:
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1443
|
+
usage:
|
|
1444
|
+
result.usage && control.usage
|
|
1445
|
+
? {
|
|
1446
|
+
inputTokens:
|
|
1447
|
+
result.usage.inputTokens + control.usage.inputTokens,
|
|
1448
|
+
costUsd: result.usage.costUsd + control.usage.costUsd,
|
|
1449
|
+
}
|
|
1450
|
+
: undefined,
|
|
858
1451
|
};
|
|
859
1452
|
}
|
|
860
1453
|
}
|
|
@@ -873,6 +1466,32 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
873
1466
|
(reversed?.ok && reversed.answers[reading.id]?.type === "unjudged"),
|
|
874
1467
|
)
|
|
875
1468
|
.map((reading) => reading.label);
|
|
1469
|
+
for (const reading of asks.readings) {
|
|
1470
|
+
for (const id of [
|
|
1471
|
+
reading.twin,
|
|
1472
|
+
...Object.values(reading.controls ?? {}),
|
|
1473
|
+
].filter((id): id is string => !!id)) {
|
|
1474
|
+
const answer = result.ok ? result.answers[id] : undefined;
|
|
1475
|
+
if (answer && answer.type !== "unjudged" && answer.source)
|
|
1476
|
+
auxiliary.controls[answer.source]++;
|
|
1477
|
+
else auxiliary.controls.notJudged++;
|
|
1478
|
+
}
|
|
1479
|
+
if (
|
|
1480
|
+
reversed &&
|
|
1481
|
+
Object.hasOwn(reversed.ok ? reversed.answers : {}, reading.id)
|
|
1482
|
+
) {
|
|
1483
|
+
const answer = reversed.ok ? reversed.answers[reading.id] : undefined;
|
|
1484
|
+
if (answer && answer.type !== "unjudged" && answer.source)
|
|
1485
|
+
auxiliary.controls[answer.source]++;
|
|
1486
|
+
else auxiliary.controls.notJudged++;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
for (const found of findResults) {
|
|
1490
|
+
const answer = found.ok ? found.answers.find : undefined;
|
|
1491
|
+
if (answer && answer.type !== "unjudged" && answer.source)
|
|
1492
|
+
auxiliary.passages[answer.source]++;
|
|
1493
|
+
else auxiliary.passages.notJudged++;
|
|
1494
|
+
}
|
|
876
1495
|
const output = finish(result, {
|
|
877
1496
|
...(result.ok
|
|
878
1497
|
? {
|
|
@@ -902,6 +1521,14 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
902
1521
|
},
|
|
903
1522
|
]
|
|
904
1523
|
: []),
|
|
1524
|
+
...(command?.ok && command.redactions
|
|
1525
|
+
? [
|
|
1526
|
+
{
|
|
1527
|
+
fact: `output: ${command.redactions} occurrence${command.redactions === 1 ? "" : "s"} of the configured Jev API key replaced with [redacted]`,
|
|
1528
|
+
next: "remove the key from the command's output; it is never sent to Jev",
|
|
1529
|
+
},
|
|
1530
|
+
]
|
|
1531
|
+
: []),
|
|
905
1532
|
...(command?.ok && command.shapeLimitExceeded
|
|
906
1533
|
? [
|
|
907
1534
|
{
|