jev-agent-tools 0.2.0 → 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 +37 -1
- package/CONTRIBUTING.md +3 -0
- package/README.md +22 -14
- package/SECURITY.md +17 -1
- package/dist/adapters/ask-files.js +11 -2
- package/dist/adapters/ask-proof.js +63 -7
- package/dist/adapters/command.js +82 -29
- package/dist/adapters/docs.js +30 -10
- package/dist/adapters/evidence-context.js +119 -0
- package/dist/adapters/files.js +141 -16
- package/dist/adapters/find.js +34 -6
- package/dist/adapters/git-base.js +7 -1
- package/dist/adapters/git.js +51 -7
- package/dist/adapters/locate-file.js +47 -9
- package/dist/adapters/private-storage.js +14 -6
- package/dist/adapters/risk-callers.js +3 -0
- package/dist/adapters/shell.js +23 -7
- package/dist/adapters/test-inventory.js +10 -2
- package/dist/configuration.js +17 -7
- package/dist/constants.js +26 -5
- package/dist/core/ask-references.js +193 -109
- package/dist/core/asks.js +78 -7
- package/dist/core/locate.js +8 -8
- package/dist/core/output.js +17 -0
- package/dist/core/result-report.js +302 -0
- package/dist/core/secret-path.js +34 -0
- package/dist/core/state.js +8 -1
- package/dist/core/units.js +1 -1
- package/dist/jev/client.js +34 -12
- package/dist/mcp/protocol.js +50 -27
- package/dist/mcp/tools.js +20 -7
- package/dist/render.js +72 -0
- package/dist/report-schema.js +1356 -0
- package/dist/result-types.js +1 -0
- package/dist/texts/ask-files.js +3 -1
- package/dist/texts/ask.js +3 -1
- package/dist/texts/check-diff.js +7 -4
- package/dist/texts/find.js +7 -2
- package/dist/texts/guide.js +3 -16
- package/dist/texts/instructions.js +72 -0
- package/dist/texts/locate.js +7 -2
- package/dist/texts/select-tests.js +3 -1
- package/dist/tools/ask-files.js +248 -15
- package/dist/tools/ask.js +523 -62
- package/dist/tools/check-diff.js +222 -30
- package/dist/tools/docs-check.js +122 -13
- package/dist/tools/find.js +320 -27
- package/dist/tools/locate.js +317 -18
- package/dist/tools/review-report.js +230 -0
- package/dist/tools/select-tests.js +273 -19
- package/dist/tools/spec-check.js +119 -22
- package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +3 -3
- package/docs/agent-instructions.md +59 -30
- package/docs/design.md +13 -1
- package/docs/mcp.md +8 -6
- 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 +1 -1
- package/rules/jev-ask.md +22 -1
- package/server.json +2 -2
- package/src/adapters/ask-files.ts +11 -3
- package/src/adapters/ask-proof.ts +69 -11
- package/src/adapters/command.ts +96 -33
- package/src/adapters/docs.ts +33 -14
- package/src/adapters/evidence-context.ts +169 -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 +17 -5
- package/src/adapters/risk-callers.ts +3 -0
- package/src/adapters/shell.ts +23 -7
- package/src/adapters/test-inventory.ts +12 -4
- package/src/configuration.ts +16 -2
- package/src/constants.ts +26 -5
- package/src/core/ask-references.ts +262 -146
- package/src/core/asks.ts +79 -7
- 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/index.ts +3 -0
- package/src/jev/client.ts +54 -16
- package/src/jev/types.ts +18 -3
- package/src/mcp/protocol.ts +91 -41
- package/src/mcp/tools.ts +26 -13
- 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/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 +309 -14
- package/src/tools/ask.ts +700 -77
- package/src/tools/check-diff.ts +331 -28
- package/src/tools/docs-check.ts +241 -39
- package/src/tools/find.ts +386 -29
- package/src/tools/locate.ts +384 -19
- package/src/tools/review-report.ts +308 -0
- package/src/tools/select-tests.ts +479 -21
- package/src/tools/spec-check.ts +193 -19
package/src/tools/ask.ts
CHANGED
|
@@ -7,6 +7,11 @@ import { collectAskRepository, repositoryPath } from "../adapters/ask-proof.ts";
|
|
|
7
7
|
import { collectProofSyntax } from "../adapters/ask-syntax.ts";
|
|
8
8
|
import { canonicalPath } from "../adapters/canonical-path.ts";
|
|
9
9
|
import { captureCommand } from "../adapters/command.ts";
|
|
10
|
+
import {
|
|
11
|
+
type EvidenceContext,
|
|
12
|
+
resolveEvidenceContext,
|
|
13
|
+
withEvidenceContext,
|
|
14
|
+
} from "../adapters/evidence-context.ts";
|
|
10
15
|
import { collectFiles } from "../adapters/files.ts";
|
|
11
16
|
import { shareGitInventory } from "../adapters/git-inventory.ts";
|
|
12
17
|
import { hostUsage } from "../adapters/usage.ts";
|
|
@@ -21,6 +26,7 @@ import {
|
|
|
21
26
|
STATE_MAX_CHARS,
|
|
22
27
|
} from "../constants.ts";
|
|
23
28
|
import { buildAskClosure } from "../core/ask-closure.ts";
|
|
29
|
+
import type { ReferenceResolution } from "../core/ask-references.ts";
|
|
24
30
|
import {
|
|
25
31
|
blockedAskReadings,
|
|
26
32
|
resolveAskReferences,
|
|
@@ -32,26 +38,57 @@ import {
|
|
|
32
38
|
type ImportSource,
|
|
33
39
|
} from "../core/imports.ts";
|
|
34
40
|
import { checkIntegrity } from "../core/integrity.ts";
|
|
35
|
-
import type {
|
|
41
|
+
import type {
|
|
42
|
+
AnswerInput,
|
|
43
|
+
BudgetRefusal,
|
|
44
|
+
EnvelopeInput,
|
|
45
|
+
} from "../core/output.ts";
|
|
36
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";
|
|
37
61
|
import { assembleState } from "../core/state.ts";
|
|
38
62
|
import { discoverTests } from "../core/test-discovery.ts";
|
|
39
63
|
import { describeAsk } from "../describe.ts";
|
|
40
64
|
import type { GuideContext } from "../guide.ts";
|
|
41
65
|
import type { Judgment } from "../jev/types.ts";
|
|
42
|
-
import {
|
|
66
|
+
import { renderResultReport } from "../render.ts";
|
|
43
67
|
import { isRecord } from "../result.ts";
|
|
44
68
|
import type { ToolDependencies } from "../runtime.ts";
|
|
45
69
|
import { COMMAND_LIMIT_NOTICE } from "../texts/ask.ts";
|
|
46
70
|
import { NOT_CONFIGURED } from "../texts/configuration.ts";
|
|
71
|
+
import { ASK_GUIDELINE } from "../texts/instructions.ts";
|
|
47
72
|
import { asksParameter } from "./ask-schema.ts";
|
|
48
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
|
+
|
|
49
87
|
export const askParameters = Type.Object(
|
|
50
88
|
{
|
|
89
|
+
root: Type.Optional(Type.String()),
|
|
51
90
|
state: Type.Optional(Type.String({ maxLength: ASK_NOTE_MAX_CHARS })),
|
|
52
|
-
paths: Type.Optional(
|
|
53
|
-
Type.Array(Type.String({ minLength: 1 }), { maxItems: ASK_MAX_FILES }),
|
|
54
|
-
),
|
|
91
|
+
paths: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
55
92
|
base: Type.Optional(Type.String({ minLength: 1 })),
|
|
56
93
|
command: Type.Optional(Type.String({ minLength: 1 })),
|
|
57
94
|
timeout_s: Type.Optional(
|
|
@@ -89,9 +126,7 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
89
126
|
: {
|
|
90
127
|
promptSnippet:
|
|
91
128
|
"Typed answers about one situation built from a note, files and a command's output",
|
|
92
|
-
promptGuidelines: [
|
|
93
|
-
"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",
|
|
94
|
-
],
|
|
129
|
+
promptGuidelines: [ASK_GUIDELINE],
|
|
95
130
|
}),
|
|
96
131
|
async execute(
|
|
97
132
|
_id: string,
|
|
@@ -101,7 +136,11 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
101
136
|
ctx: { cwd: string } & GuideContext,
|
|
102
137
|
): Promise<{
|
|
103
138
|
content: { type: "text"; text: string }[];
|
|
104
|
-
details: Judgment
|
|
139
|
+
details: Judgment & {
|
|
140
|
+
result: ResultReportV1;
|
|
141
|
+
evidenceContext: EvidenceContext;
|
|
142
|
+
evidenceFacts: AskEvidenceFacts;
|
|
143
|
+
};
|
|
105
144
|
usage?: {
|
|
106
145
|
input: number;
|
|
107
146
|
output: number;
|
|
@@ -118,8 +157,46 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
118
157
|
};
|
|
119
158
|
}> {
|
|
120
159
|
const client = dependencies.client;
|
|
121
|
-
const
|
|
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;
|
|
122
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[] = [];
|
|
123
200
|
const exec = shareGitInventory(execute);
|
|
124
201
|
const finish = (
|
|
125
202
|
result: Judgment,
|
|
@@ -137,15 +214,324 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
137
214
|
},
|
|
138
215
|
});
|
|
139
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
|
+
});
|
|
140
491
|
runtime.guide.deliver(ctx);
|
|
141
492
|
return {
|
|
142
|
-
content: [
|
|
143
|
-
|
|
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
|
+
},
|
|
144
505
|
};
|
|
145
506
|
};
|
|
146
|
-
const textResult = (text: string) =>
|
|
147
|
-
|
|
148
|
-
|
|
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");
|
|
149
535
|
if (args.command && !allowCommand)
|
|
150
536
|
return textResult("command disabled by JEV_TOOLS_ALLOW_COMMAND=0");
|
|
151
537
|
if (
|
|
@@ -159,21 +545,23 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
159
545
|
);
|
|
160
546
|
if (args.state === undefined && !args.paths?.length && !args.command)
|
|
161
547
|
return textResult("Provide state, command or at least one path.");
|
|
162
|
-
const asks = compileAsks(args.asks, { surface: "ask" });
|
|
163
|
-
if (!asks.ok) return textResult(asks.error);
|
|
164
548
|
const files = await collectFiles(cwd, args.paths ?? [], signal, {
|
|
165
549
|
exec,
|
|
166
550
|
allowEmpty: true,
|
|
167
551
|
skipInvalidUtf8: true,
|
|
168
552
|
});
|
|
169
|
-
if (!files.ok)
|
|
553
|
+
if (!files.ok)
|
|
554
|
+
return textResult(files.error, files.cause ?? "file_unavailable");
|
|
170
555
|
if (
|
|
171
556
|
!Object.keys(files.files).length &&
|
|
172
557
|
args.state === undefined &&
|
|
173
558
|
!args.command &&
|
|
174
559
|
files.skipped.length
|
|
175
560
|
)
|
|
176
|
-
return textResult(
|
|
561
|
+
return textResult(
|
|
562
|
+
`No readable evidence: ${files.skipped.join("; ")}`,
|
|
563
|
+
"collection_empty",
|
|
564
|
+
);
|
|
177
565
|
const proofLimitations: Array<
|
|
178
566
|
NonNullable<EnvelopeInput["limitations"]>[number]
|
|
179
567
|
> = files.skipped.map((fact) => ({
|
|
@@ -181,12 +569,24 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
181
569
|
next: "Provide this file as UTF-8 text if the judgment needs it.",
|
|
182
570
|
}));
|
|
183
571
|
let repository = await collectAskRepository(exec, cwd, args.base, signal);
|
|
184
|
-
|
|
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");
|
|
185
579
|
if (!repository.ok)
|
|
186
580
|
proofLimitations.push({
|
|
187
581
|
fact: `closure unavailable: ${repository.error}`,
|
|
188
582
|
next: "pass dependencies explicitly in paths",
|
|
189
583
|
});
|
|
584
|
+
if (args.command)
|
|
585
|
+
commandContext = {
|
|
586
|
+
...commandContext,
|
|
587
|
+
execution: "started",
|
|
588
|
+
cwd: known(repository.ok ? repository.root : cwd),
|
|
589
|
+
};
|
|
190
590
|
const command = args.command
|
|
191
591
|
? await captureCommand(
|
|
192
592
|
exec,
|
|
@@ -194,18 +594,49 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
194
594
|
args.command,
|
|
195
595
|
args.timeout_s,
|
|
196
596
|
signal,
|
|
597
|
+
dependencies.apiKey ?? process.env.JEV_TOOLS_API_KEY,
|
|
197
598
|
)
|
|
198
599
|
: undefined;
|
|
199
|
-
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
|
+
}
|
|
200
629
|
if (command?.ok) {
|
|
201
630
|
repository = await collectAskRepository(exec, cwd, args.base, signal);
|
|
202
|
-
if (!repository.ok && args.base)
|
|
631
|
+
if (!repository.ok && args.base)
|
|
632
|
+
return textResult(repository.error, "invalid_base");
|
|
203
633
|
}
|
|
204
634
|
const initialState = assembleState(args.state, files.files);
|
|
205
635
|
if (!initialState.ok) return textResult(initialState.error);
|
|
206
636
|
if (command?.ok && Object.hasOwn(initialState.state, "output"))
|
|
207
637
|
return textResult(
|
|
208
638
|
"state contains reserved key output when command is supplied; rename it in your note",
|
|
639
|
+
"reserved_key",
|
|
209
640
|
);
|
|
210
641
|
let outputNotIdentifiable = false;
|
|
211
642
|
if (command?.ok && repository.ok) {
|
|
@@ -309,35 +740,75 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
309
740
|
const references = resolveAskReferences(
|
|
310
741
|
asks,
|
|
311
742
|
args.state,
|
|
312
|
-
|
|
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,
|
|
313
754
|
repository?.ok ? [...repository.known] : [],
|
|
314
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
|
+
);
|
|
315
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
|
+
}
|
|
316
783
|
if (repository?.ok) {
|
|
317
784
|
const root = repository.root;
|
|
318
785
|
const additions = await Promise.all(
|
|
319
|
-
references.additions
|
|
320
|
-
addition
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
+
})),
|
|
327
796
|
);
|
|
328
797
|
for (const { addition, added } of additions) {
|
|
329
798
|
if (added.ok) {
|
|
330
|
-
|
|
799
|
+
const local = repositoryPath(cwd, root, addition.path);
|
|
800
|
+
files.files[local] = added.files[addition.path] ?? "";
|
|
331
801
|
files.identities.push(
|
|
332
802
|
...added.identities.map((identity) => ({
|
|
333
803
|
...identity,
|
|
334
|
-
requestedPath:
|
|
804
|
+
requestedPath: local,
|
|
335
805
|
})),
|
|
336
806
|
);
|
|
337
807
|
} else
|
|
338
808
|
unresolved.push({
|
|
339
|
-
|
|
809
|
+
...addition,
|
|
340
810
|
reason: added.error,
|
|
811
|
+
...(added.cause ? { cause: added.cause } : {}),
|
|
341
812
|
});
|
|
342
813
|
}
|
|
343
814
|
}
|
|
@@ -346,44 +817,58 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
346
817
|
if (command?.ok && Object.hasOwn(state.state, "output"))
|
|
347
818
|
return textResult(
|
|
348
819
|
"state contains reserved key output when command is supplied; rename it in your note",
|
|
820
|
+
"reserved_key",
|
|
349
821
|
);
|
|
350
822
|
if (command?.ok) state.state.output = { ...command.output };
|
|
351
823
|
if (repository?.ok) {
|
|
824
|
+
if (evidenceContext.effectiveRoot)
|
|
825
|
+
evidenceContext.effectiveRoot.path = repository.root;
|
|
826
|
+
evidenceContext.requestedBase = args.base;
|
|
827
|
+
evidenceContext.resolvedBase = repository.baseSha;
|
|
352
828
|
const canonical = Object.fromEntries(
|
|
353
829
|
Object.entries(files.files).map(([path, text]) => [
|
|
354
830
|
repositoryPath(repository.root, cwd, path),
|
|
355
831
|
text,
|
|
356
832
|
]),
|
|
357
833
|
);
|
|
358
|
-
|
|
359
|
-
|
|
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.
|
|
360
849
|
const sources: ImportSource[] = Object.entries(canonical).map(
|
|
361
850
|
([path, text]) => ({ path, text }),
|
|
362
851
|
);
|
|
363
|
-
const before: Record<string, string | null> = {};
|
|
852
|
+
const before: Record<string, string | null> = { ...historical };
|
|
364
853
|
if (args.base) {
|
|
365
854
|
for (const path of Object.keys(canonical)) {
|
|
366
855
|
const old = await repository.readBefore(path);
|
|
367
|
-
if (!old.ok)
|
|
856
|
+
if (!old.ok)
|
|
857
|
+
return textResult(old.error, old.cause ?? "file_unavailable");
|
|
368
858
|
before[path] = old.text;
|
|
369
859
|
}
|
|
370
860
|
state.state.base_sha = repository.baseSha ?? "";
|
|
371
|
-
state.state.files_before =
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
(addition) => addition.reference === path,
|
|
376
|
-
)?.path ?? repositoryPath(repository.root, cwd, path);
|
|
377
|
-
return [path, before[canonicalPath] ?? null];
|
|
378
|
-
}),
|
|
379
|
-
);
|
|
380
|
-
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;
|
|
381
865
|
if (stateSize > STATE_MAX_CHARS) {
|
|
382
866
|
const partSizes = Object.entries(state.state)
|
|
383
867
|
.map(([part, value]) => `${part} ${JSON.stringify(value).length}`)
|
|
384
868
|
.join(", ");
|
|
385
869
|
return textResult(
|
|
386
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",
|
|
387
872
|
);
|
|
388
873
|
}
|
|
389
874
|
}
|
|
@@ -483,14 +968,17 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
483
968
|
sources,
|
|
484
969
|
...(args.base ? { before } : {}),
|
|
485
970
|
};
|
|
971
|
+
const closureState = { ...state.state };
|
|
972
|
+
if (command?.ok) delete closureState.output;
|
|
486
973
|
const closure = buildAskClosure({
|
|
487
974
|
graph: { edges, limits },
|
|
488
975
|
syntax,
|
|
489
976
|
paths: Object.keys(canonical),
|
|
490
977
|
intentions: JSON.stringify(asks.asks),
|
|
491
|
-
state:
|
|
978
|
+
state: closureState,
|
|
492
979
|
});
|
|
493
980
|
state.state = closure.state;
|
|
981
|
+
if (command?.ok) state.state.output = { ...command.output };
|
|
494
982
|
proofLimitations.push(
|
|
495
983
|
...closure.limitations.filter(
|
|
496
984
|
(limit) => !limit.fact.startsWith("Import closure only,"),
|
|
@@ -505,6 +993,22 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
505
993
|
fact: "closure: +0; imports only, depth 1; repository inventory unavailable",
|
|
506
994
|
next: "pass dependencies explicitly in paths",
|
|
507
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;
|
|
508
1012
|
const blocked = blockedAskReadings(
|
|
509
1013
|
asks,
|
|
510
1014
|
state.state,
|
|
@@ -513,13 +1017,16 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
513
1017
|
);
|
|
514
1018
|
const inserted = isRecord(state.state.files) ? state.state.files : {};
|
|
515
1019
|
const identities = files.identities.map((file) => {
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
1020
|
+
const insertedPath = repository.ok
|
|
1021
|
+
? repositoryPath(repository.root, cwd, file.requestedPath)
|
|
1022
|
+
: file.requestedPath;
|
|
1023
|
+
const content = inserted[insertedPath];
|
|
520
1024
|
return {
|
|
521
1025
|
...file,
|
|
522
|
-
insertedPath:
|
|
1026
|
+
insertedPath: resolve(
|
|
1027
|
+
repository.ok ? repository.root : cwd,
|
|
1028
|
+
insertedPath,
|
|
1029
|
+
),
|
|
523
1030
|
content: typeof content === "string" ? content : "",
|
|
524
1031
|
insertedSha256: createHash("sha256")
|
|
525
1032
|
.update(typeof content === "string" ? content : "")
|
|
@@ -531,6 +1038,24 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
531
1038
|
.filter((group) => group.some((id) => blocked.has(id)))
|
|
532
1039
|
.flat(),
|
|
533
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
|
+
});
|
|
534
1059
|
const questions = Object.fromEntries(
|
|
535
1060
|
Object.entries(asks.questions).filter(
|
|
536
1061
|
([id]) => !blockedMembers.has(id),
|
|
@@ -543,11 +1068,14 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
543
1068
|
);
|
|
544
1069
|
let sent = 0;
|
|
545
1070
|
let refusal: BudgetRefusal | undefined;
|
|
546
|
-
const findResults: Judgment[] = [];
|
|
547
1071
|
const options = {
|
|
548
1072
|
signal,
|
|
549
1073
|
cache: !args.command,
|
|
550
1074
|
...runtime.session.requestGate(),
|
|
1075
|
+
admissionCause: () =>
|
|
1076
|
+
refusal?.kind === "session"
|
|
1077
|
+
? ("session_budget" as const)
|
|
1078
|
+
: ("call_budget" as const),
|
|
551
1079
|
beforeRequest: (questionCount: number) => {
|
|
552
1080
|
if (args.max_calls !== undefined && sent >= args.max_calls) {
|
|
553
1081
|
refusal = {
|
|
@@ -582,11 +1110,25 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
582
1110
|
)
|
|
583
1111
|
return textResult(
|
|
584
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",
|
|
585
1127
|
);
|
|
586
1128
|
const found = await Promise.all(
|
|
587
|
-
|
|
1129
|
+
chunkStates.map(async (chunkState) => {
|
|
588
1130
|
const judgment = await client.judge(
|
|
589
|
-
|
|
1131
|
+
chunkState,
|
|
590
1132
|
{
|
|
591
1133
|
find: {
|
|
592
1134
|
type: "bool",
|
|
@@ -620,6 +1162,7 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
620
1162
|
if (budget < 100)
|
|
621
1163
|
return textResult(
|
|
622
1164
|
"No space for command output; reduce paths or state.",
|
|
1165
|
+
"evidence_too_large",
|
|
623
1166
|
);
|
|
624
1167
|
const stderrSize = JSON.stringify(output.stderr).length;
|
|
625
1168
|
const stdoutSize = JSON.stringify(output.stdout).length;
|
|
@@ -682,6 +1225,7 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
682
1225
|
if (JSON.stringify(state.state).length > STATE_MAX_CHARS)
|
|
683
1226
|
return textResult(
|
|
684
1227
|
"serialized command output exceeds state budget; narrow command or reduce paths",
|
|
1228
|
+
"evidence_too_large",
|
|
685
1229
|
);
|
|
686
1230
|
if (outputNotIdentifiable)
|
|
687
1231
|
integrity.controls.push({
|
|
@@ -734,13 +1278,14 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
734
1278
|
cacheHits: (first.cacheHits ?? 0) + (reversed.cacheHits ?? 0),
|
|
735
1279
|
cacheRequests:
|
|
736
1280
|
(first.cacheRequests ?? 0) + (reversed.cacheRequests ?? 0),
|
|
737
|
-
usage:
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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,
|
|
744
1289
|
};
|
|
745
1290
|
}
|
|
746
1291
|
}
|
|
@@ -749,12 +1294,17 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
749
1294
|
...result,
|
|
750
1295
|
calls: (result.calls ?? 0) + (found.calls ?? 0),
|
|
751
1296
|
questions: (result.questions ?? 0) + (found.questions ?? 0),
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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,
|
|
758
1308
|
};
|
|
759
1309
|
const attribution: Array<
|
|
760
1310
|
NonNullable<EnvelopeInput["limitations"]>[number]
|
|
@@ -779,6 +1329,8 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
779
1329
|
fact: `attribution ${reading.label}: no designated candidate`,
|
|
780
1330
|
next: "provide decisive candidate evidence before acting",
|
|
781
1331
|
});
|
|
1332
|
+
attributionMissing.add(reading.id);
|
|
1333
|
+
auxiliary.controls.notJudged++;
|
|
782
1334
|
continue;
|
|
783
1335
|
}
|
|
784
1336
|
if (!Object.hasOwn(inserted, selected)) {
|
|
@@ -786,6 +1338,8 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
786
1338
|
fact: `attribution ${reading.label}: designated piece ${selected} is absent`,
|
|
787
1339
|
next: `add ${selected} to paths before acting`,
|
|
788
1340
|
});
|
|
1341
|
+
attributionMissing.add(reading.id);
|
|
1342
|
+
auxiliary.controls.notJudged++;
|
|
789
1343
|
continue;
|
|
790
1344
|
}
|
|
791
1345
|
const selectedPath = repository?.ok
|
|
@@ -834,6 +1388,40 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
834
1388
|
options,
|
|
835
1389
|
);
|
|
836
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++;
|
|
837
1425
|
attribution.push(
|
|
838
1426
|
answer?.type === "choice"
|
|
839
1427
|
? {
|
|
@@ -852,13 +1440,14 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
852
1440
|
cacheHits: (result.cacheHits ?? 0) + (control.cacheHits ?? 0),
|
|
853
1441
|
cacheRequests:
|
|
854
1442
|
(result.cacheRequests ?? 0) + (control.cacheRequests ?? 0),
|
|
855
|
-
usage:
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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,
|
|
862
1451
|
};
|
|
863
1452
|
}
|
|
864
1453
|
}
|
|
@@ -877,6 +1466,32 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
877
1466
|
(reversed?.ok && reversed.answers[reading.id]?.type === "unjudged"),
|
|
878
1467
|
)
|
|
879
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
|
+
}
|
|
880
1495
|
const output = finish(result, {
|
|
881
1496
|
...(result.ok
|
|
882
1497
|
? {
|
|
@@ -906,6 +1521,14 @@ export function createAskTool(dependencies: ToolDependencies) {
|
|
|
906
1521
|
},
|
|
907
1522
|
]
|
|
908
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
|
+
: []),
|
|
909
1532
|
...(command?.ok && command.shapeLimitExceeded
|
|
910
1533
|
? [
|
|
911
1534
|
{
|