@namzu/sdk 62.12.0 → 62.13.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 +23 -0
- package/dist/connector/index.d.ts +1 -1
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/connector/index.js.map +1 -1
- package/dist/connector/mcp/client.d.ts +96 -0
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +488 -13
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/connector/mcp/index.d.ts +1 -0
- package/dist/connector/mcp/index.d.ts.map +1 -1
- package/dist/connector/mcp/index.js +1 -0
- package/dist/connector/mcp/index.js.map +1 -1
- package/dist/connector/mcp/tasks.d.ts +40 -0
- package/dist/connector/mcp/tasks.d.ts.map +1 -0
- package/dist/connector/mcp/tasks.js +55 -0
- package/dist/connector/mcp/tasks.js.map +1 -0
- package/dist/constants/mcp/index.d.ts +1 -1
- package/dist/constants/mcp/index.d.ts.map +1 -1
- package/dist/constants/mcp/index.js +5 -0
- package/dist/constants/mcp/index.js.map +1 -1
- package/dist/eval/bootstrap.d.ts +122 -0
- package/dist/eval/bootstrap.d.ts.map +1 -0
- package/dist/eval/bootstrap.js +227 -0
- package/dist/eval/bootstrap.js.map +1 -0
- package/dist/eval/compare.d.ts +109 -0
- package/dist/eval/compare.d.ts.map +1 -0
- package/dist/eval/compare.js +324 -0
- package/dist/eval/compare.js.map +1 -0
- package/dist/eval/disclosure.d.ts +70 -0
- package/dist/eval/disclosure.d.ts.map +1 -0
- package/dist/eval/disclosure.js +64 -0
- package/dist/eval/disclosure.js.map +1 -0
- package/dist/eval/experiment.d.ts +23 -0
- package/dist/eval/experiment.d.ts.map +1 -1
- package/dist/eval/experiment.js +87 -15
- package/dist/eval/experiment.js.map +1 -1
- package/dist/eval/from-turn.d.ts.map +1 -1
- package/dist/eval/from-turn.js +5 -0
- package/dist/eval/from-turn.js.map +1 -1
- package/dist/eval/index.d.ts +13 -2
- package/dist/eval/index.d.ts.map +1 -1
- package/dist/eval/index.js +6 -1
- package/dist/eval/index.js.map +1 -1
- package/dist/eval/report-stats.d.ts +77 -0
- package/dist/eval/report-stats.d.ts.map +1 -0
- package/dist/eval/report-stats.js +184 -0
- package/dist/eval/report-stats.js.map +1 -0
- package/dist/eval/run-options.d.ts +30 -0
- package/dist/eval/run-options.d.ts.map +1 -0
- package/dist/eval/run-options.js +20 -0
- package/dist/eval/run-options.js.map +1 -0
- package/dist/eval/run-summary.d.ts +36 -0
- package/dist/eval/run-summary.d.ts.map +1 -0
- package/dist/eval/run-summary.js +57 -0
- package/dist/eval/run-summary.js.map +1 -0
- package/dist/eval/scorers.d.ts +32 -0
- package/dist/eval/scorers.d.ts.map +1 -1
- package/dist/eval/scorers.js +77 -0
- package/dist/eval/scorers.js.map +1 -1
- package/dist/eval/types.d.ts +74 -0
- package/dist/eval/types.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts +3 -0
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +18 -2
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/public-runtime.d.ts +5 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +7 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +2 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/registry/command/kernel-commands.d.ts +3 -0
- package/dist/registry/command/kernel-commands.d.ts.map +1 -1
- package/dist/registry/command/kernel-commands.js +76 -5
- package/dist/registry/command/kernel-commands.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +15 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +18 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +80 -1
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +18 -1
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/turn/answer-claims.d.ts +52 -0
- package/dist/runtime/query/turn/answer-claims.d.ts.map +1 -0
- package/dist/runtime/query/turn/answer-claims.js +348 -0
- package/dist/runtime/query/turn/answer-claims.js.map +1 -0
- package/dist/runtime/query/turn/outcome-ledger.d.ts +7 -0
- package/dist/runtime/query/turn/outcome-ledger.d.ts.map +1 -1
- package/dist/runtime/query/turn/outcome-ledger.js +13 -0
- package/dist/runtime/query/turn/outcome-ledger.js.map +1 -1
- package/dist/store/goal/index.d.ts +3 -1
- package/dist/store/goal/index.d.ts.map +1 -1
- package/dist/store/goal/index.js +6 -0
- package/dist/store/goal/index.js.map +1 -1
- package/dist/telemetry/runtime-accessors.d.ts +14 -0
- package/dist/telemetry/runtime-accessors.d.ts.map +1 -1
- package/dist/telemetry/runtime-accessors.js +32 -0
- package/dist/telemetry/runtime-accessors.js.map +1 -1
- package/dist/tools/goal/confirmation.d.ts +154 -0
- package/dist/tools/goal/confirmation.d.ts.map +1 -0
- package/dist/tools/goal/confirmation.js +290 -0
- package/dist/tools/goal/confirmation.js.map +1 -0
- package/dist/tools/goal/index.d.ts +4 -1
- package/dist/tools/goal/index.d.ts.map +1 -1
- package/dist/tools/goal/index.js +27 -2
- package/dist/tools/goal/index.js.map +1 -1
- package/dist/tools/hand-back/index.d.ts +48 -0
- package/dist/tools/hand-back/index.d.ts.map +1 -0
- package/dist/tools/hand-back/index.js +129 -0
- package/dist/tools/hand-back/index.js.map +1 -0
- package/dist/tools/schedules/schedule-tool.d.ts.map +1 -1
- package/dist/tools/schedules/schedule-tool.js +15 -1
- package/dist/tools/schedules/schedule-tool.js.map +1 -1
- package/dist/tools/schedules/types.d.ts +10 -0
- package/dist/tools/schedules/types.d.ts.map +1 -1
- package/dist/types/connector/mcp.d.ts +18 -0
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/goal/index.d.ts +12 -0
- package/dist/types/goal/index.d.ts.map +1 -1
- package/dist/types/session/config.d.ts +8 -0
- package/dist/types/session/config.d.ts.map +1 -1
- package/dist/types/session/hand-back.d.ts +29 -0
- package/dist/types/session/hand-back.d.ts.map +1 -0
- package/dist/types/session/hand-back.js +11 -0
- package/dist/types/session/hand-back.js.map +1 -0
- package/dist/types/session/outcome.d.ts +37 -0
- package/dist/types/session/outcome.d.ts.map +1 -1
- package/dist/types/session/outcome.js.map +1 -1
- package/dist/types/session/turn.d.ts +19 -1
- package/dist/types/session/turn.d.ts.map +1 -1
- package/dist/types/session/turn.js.map +1 -1
- package/package.json +1 -1
- package/src/connector/index.ts +1 -0
- package/src/connector/mcp/client.ts +531 -14
- package/src/connector/mcp/index.ts +1 -0
- package/src/connector/mcp/tasks.ts +81 -0
- package/src/constants/mcp/index.ts +6 -1
- package/src/eval/bootstrap.ts +314 -0
- package/src/eval/compare.ts +473 -0
- package/src/eval/disclosure.ts +136 -0
- package/src/eval/experiment.ts +127 -15
- package/src/eval/from-turn.ts +5 -0
- package/src/eval/index.ts +51 -0
- package/src/eval/report-stats.ts +257 -0
- package/src/eval/run-options.ts +49 -0
- package/src/eval/run-summary.ts +104 -0
- package/src/eval/scorers.ts +83 -0
- package/src/eval/types.ts +76 -0
- package/src/manager/session/turn-recorder.ts +23 -2
- package/src/public-runtime.ts +53 -0
- package/src/public-types.ts +9 -0
- package/src/registry/command/kernel-commands.ts +94 -6
- package/src/runtime/query/index.ts +18 -0
- package/src/runtime/query/iteration/index.ts +85 -1
- package/src/runtime/query/iteration/phases/tool-review.ts +24 -1
- package/src/runtime/query/turn/answer-claims.ts +413 -0
- package/src/runtime/query/turn/outcome-ledger.ts +12 -0
- package/src/store/goal/index.ts +11 -0
- package/src/telemetry/runtime-accessors.ts +29 -0
- package/src/tools/goal/confirmation.ts +375 -0
- package/src/tools/goal/index.ts +37 -1
- package/src/tools/hand-back/index.ts +150 -0
- package/src/tools/schedules/schedule-tool.ts +19 -1
- package/src/tools/schedules/types.ts +10 -0
- package/src/types/connector/mcp.ts +18 -0
- package/src/types/goal/index.ts +13 -0
- package/src/types/session/config.ts +8 -0
- package/src/types/session/hand-back.ts +31 -0
- package/src/types/session/outcome.ts +42 -0
- package/src/types/session/turn.ts +19 -1
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
} from '../../../telemetry/attributes.js'
|
|
30
30
|
import { getTracer } from '../../../telemetry/runtime-accessors.js'
|
|
31
31
|
import { STRUCTURED_OUTPUT_TOOL_NAME } from '../../../tools/builtins/structuredOutput.js'
|
|
32
|
+
import { SESSION_GOAL_TOOL_NAMES } from '../../../tools/goal/index.js'
|
|
33
|
+
import { HAND_BACK_TOOL_NAME, parseHandBack } from '../../../tools/hand-back/index.js'
|
|
32
34
|
import type { ToolManager } from '../../../toolsets/manager.js'
|
|
33
35
|
import { RECALLED_MEMORY_ENTRY_KEY, recalledMemoryIds } from '../../../turn/memory-recall.js'
|
|
34
36
|
import { type CostInfo, type TokenUsage, promptSizeOf } from '../../../types/common/index.js'
|
|
@@ -48,6 +50,7 @@ import {
|
|
|
48
50
|
import { classifyProviderError, readOutputOverflow } from '../../../types/provider/errors.js'
|
|
49
51
|
import type { ChatCompletionResponse, ReasoningEffort } from '../../../types/provider/index.js'
|
|
50
52
|
import type { AnswerReview, AnswerReviewContext } from '../../../types/session/answer-review.js'
|
|
53
|
+
import type { HandBack } from '../../../types/session/hand-back.js'
|
|
51
54
|
import type {
|
|
52
55
|
RecalledMemory,
|
|
53
56
|
SessionEvent,
|
|
@@ -136,6 +139,9 @@ import {
|
|
|
136
139
|
} from './step-shaping.js'
|
|
137
140
|
import { streamProviderTurn } from './stream-turn.js'
|
|
138
141
|
|
|
142
|
+
/** Notes a host may add before a turn ends (`TurnConfig.outcome.beforeEnd`) in one turn. */
|
|
143
|
+
const MAX_BEFORE_END_NOTES = 16
|
|
144
|
+
|
|
139
145
|
/** Capped steps in a row after which the per-step cap stops applying for the turn's next step. */
|
|
140
146
|
const MAX_CAPPED_STREAK = 3
|
|
141
147
|
|
|
@@ -339,6 +345,7 @@ export class IterationOrchestrator {
|
|
|
339
345
|
private readonly completionAudit?: CompletionAudit
|
|
340
346
|
/** What the turn changed and what checked it; see {@link OutcomeLedger}. Kept whether or not the host asked. */
|
|
341
347
|
private readonly outcomeLedger: OutcomeLedger
|
|
348
|
+
private beforeEndNotes = 0
|
|
342
349
|
private outcomeHydrated = false
|
|
343
350
|
/** The next step must call a tool, at low effort: set by the stall monitor or a capped step. */
|
|
344
351
|
private forceActionNextStep = false
|
|
@@ -2016,6 +2023,14 @@ export class IterationOrchestrator {
|
|
|
2016
2023
|
recorder.pushMessage(createRuntimeContextMessage(auditReminder, 'progress-nudge'))
|
|
2017
2024
|
continue
|
|
2018
2025
|
}
|
|
2026
|
+
const endNote =
|
|
2027
|
+
!forceFinalize && closingStopReason === undefined
|
|
2028
|
+
? await this.askBeforeEnd()
|
|
2029
|
+
: undefined
|
|
2030
|
+
if (endNote !== undefined) {
|
|
2031
|
+
recorder.pushMessage(createRuntimeContextMessage(endNote, 'progress-nudge'))
|
|
2032
|
+
continue
|
|
2033
|
+
}
|
|
2019
2034
|
if (!forceFinalize && closingStopReason === undefined) {
|
|
2020
2035
|
const feedback = yield* this.askStopHooks()
|
|
2021
2036
|
const stoppedByHook = this.ctx.toolExecutor.takeHalt?.()
|
|
@@ -2074,6 +2089,28 @@ export class IterationOrchestrator {
|
|
|
2074
2089
|
if (reviewOutcome.decision === 'stop') {
|
|
2075
2090
|
return
|
|
2076
2091
|
}
|
|
2092
|
+
|
|
2093
|
+
// The model handed the task back to a person. Nothing else ran in
|
|
2094
|
+
// this step (its sibling calls were refused), and no further model
|
|
2095
|
+
// call is made: the turn ends here, as a turn that ended on its own.
|
|
2096
|
+
const handedBack = this.takeHandBack(reviewOutcome.results, response)
|
|
2097
|
+
if (handedBack !== undefined) {
|
|
2098
|
+
this.ctx.log.info('The model handed the task back — ending turn', {
|
|
2099
|
+
[NAMZU.TURN_ID]: recorder.turnId,
|
|
2100
|
+
[NAMZU.ITERATION]: iterationNum,
|
|
2101
|
+
'namzu.hand_back.kind': handedBack.kind,
|
|
2102
|
+
})
|
|
2103
|
+
recorder.setHandedBack(handedBack)
|
|
2104
|
+
recorder.setStopReason('end_turn')
|
|
2105
|
+
await this.ctx.emitEvent({
|
|
2106
|
+
type: 'iteration_completed',
|
|
2107
|
+
turnId: recorder.turnId,
|
|
2108
|
+
iteration: iterationNum,
|
|
2109
|
+
hasToolCalls: true,
|
|
2110
|
+
})
|
|
2111
|
+
yield* this.ctx.drainPending()
|
|
2112
|
+
break
|
|
2113
|
+
}
|
|
2077
2114
|
const needsCorrection = await this.recordToolStructuredFailure(
|
|
2078
2115
|
reviewOutcome.results,
|
|
2079
2116
|
iterationNum,
|
|
@@ -3406,8 +3443,14 @@ export class IterationOrchestrator {
|
|
|
3406
3443
|
else this.outcomeLedger.restore(restored.snapshot)
|
|
3407
3444
|
}
|
|
3408
3445
|
|
|
3409
|
-
/**
|
|
3446
|
+
/**
|
|
3447
|
+
* Whether a call is one the outcome record may treat as only looking. The
|
|
3448
|
+
* goal tools write the goal's own record, never the workspace; counting a
|
|
3449
|
+
* report of completion as a possible change would make every check before it
|
|
3450
|
+
* read as not known to be fresh.
|
|
3451
|
+
*/
|
|
3410
3452
|
private lookedOnly(toolName: string, input: unknown): boolean {
|
|
3453
|
+
if ((SESSION_GOAL_TOOL_NAMES as readonly string[]).includes(toolName)) return true
|
|
3411
3454
|
try {
|
|
3412
3455
|
return !callChangedState(toolName, this.ctx.tools.get(toolName)?.isReadOnly?.(input), false)
|
|
3413
3456
|
} catch {
|
|
@@ -3474,6 +3517,47 @@ export class IterationOrchestrator {
|
|
|
3474
3517
|
}
|
|
3475
3518
|
}
|
|
3476
3519
|
|
|
3520
|
+
/**
|
|
3521
|
+
* The host's say before the turn ends (`TurnConfig.outcome.beforeEnd`): a note
|
|
3522
|
+
* for the model, or `undefined` to let the turn end. Bounded, and a hook that
|
|
3523
|
+
* fails never fails the turn.
|
|
3524
|
+
*/
|
|
3525
|
+
async askBeforeEnd(): Promise<string | undefined> {
|
|
3526
|
+
const hook = this.ctx.turnConfig.outcome?.beforeEnd
|
|
3527
|
+
if (hook === undefined || this.beforeEndNotes >= MAX_BEFORE_END_NOTES) return undefined
|
|
3528
|
+
try {
|
|
3529
|
+
const note = await hook(async () => {
|
|
3530
|
+
this.hydrateOutcome()
|
|
3531
|
+
return await this.outcomeLedger.preview()
|
|
3532
|
+
})
|
|
3533
|
+
if (typeof note !== 'string' || note.trim() === '') return undefined
|
|
3534
|
+
this.beforeEndNotes += 1
|
|
3535
|
+
return note
|
|
3536
|
+
} catch (err) {
|
|
3537
|
+
this.ctx.log.warn('The host hook before the turn ends failed; the turn ends', {
|
|
3538
|
+
'exception.message': toErrorMessage(err),
|
|
3539
|
+
})
|
|
3540
|
+
return undefined
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
/**
|
|
3545
|
+
* The hand-back this step made, when the setting is on and `hand_back_task`
|
|
3546
|
+
* succeeded; the first one when the model made several (the rest were refused).
|
|
3547
|
+
*/
|
|
3548
|
+
private takeHandBack(
|
|
3549
|
+
results: readonly ToolCallOutcome[],
|
|
3550
|
+
response: ChatCompletionResponse,
|
|
3551
|
+
): HandBack | undefined {
|
|
3552
|
+
if (this.ctx.turnConfig.handBack !== true) return undefined
|
|
3553
|
+
const hit = results.find(
|
|
3554
|
+
(r) => r.toolName === HAND_BACK_TOOL_NAME && r.isError !== true && r.skipped !== true,
|
|
3555
|
+
)
|
|
3556
|
+
if (!hit) return undefined
|
|
3557
|
+
const call = (response.message.toolCalls ?? []).find((c) => c.id === hit.toolCallId)
|
|
3558
|
+
return call ? parseHandBack(call.function.arguments) : undefined
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3477
3561
|
/** The outcome record as the turn leaves it; `undefined` when none was kept. */
|
|
3478
3562
|
async takeOutcome(): Promise<TurnOutcome | undefined> {
|
|
3479
3563
|
try {
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { allowRuleScope } from '../../../../authorization/gate.js'
|
|
2
2
|
import type { AuthorizationGate } from '../../../../authorization/index.js'
|
|
3
|
+
import {
|
|
4
|
+
HAND_BACK_SIBLING_REFUSAL,
|
|
5
|
+
HAND_BACK_TOOL_NAME,
|
|
6
|
+
parseHandBack,
|
|
7
|
+
} from '../../../../tools/hand-back/index.js'
|
|
3
8
|
import { isTrustedReadOnly } from '../../../../tools/trusted-read-only.js'
|
|
4
9
|
import type { ToolSourceRef } from '../../../../toolsets/types.js'
|
|
5
10
|
import type {
|
|
@@ -259,7 +264,25 @@ export async function* runToolReview(
|
|
|
259
264
|
* with the tracker's refusal instead of being run — the one denial the
|
|
260
265
|
* tracker makes, on top of whatever the caller already denied.
|
|
261
266
|
*/
|
|
262
|
-
const withRepeatRefusals = (
|
|
267
|
+
const withRepeatRefusals = (given?: ToolCallDenials): ToolCallDenials | undefined => {
|
|
268
|
+
// A step that hands the task back runs nothing else: the first hand-back
|
|
269
|
+
// call is the step, every other call in it is refused.
|
|
270
|
+
let denials = given
|
|
271
|
+
if (ctx.turnConfig?.handBack === true) {
|
|
272
|
+
const handBack = toolCallSummaries.find(
|
|
273
|
+
(summary) =>
|
|
274
|
+
summary.name === HAND_BACK_TOOL_NAME && parseHandBack(summary.input) !== undefined,
|
|
275
|
+
)
|
|
276
|
+
if (handBack) {
|
|
277
|
+
let siblings: Map<string, string> | undefined
|
|
278
|
+
for (const summary of toolCallSummaries) {
|
|
279
|
+
if (summary.id === handBack.id || given?.has(summary.id)) continue
|
|
280
|
+
siblings ??= new Map(given ?? [])
|
|
281
|
+
siblings.set(summary.id, HAND_BACK_SIBLING_REFUSAL)
|
|
282
|
+
}
|
|
283
|
+
denials = siblings ?? given
|
|
284
|
+
}
|
|
285
|
+
}
|
|
263
286
|
const tracker = ctx.repeatCalls
|
|
264
287
|
if (!tracker) return denials
|
|
265
288
|
let merged: Map<string, string> | undefined
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare what an answer claims about checks with what the turn's outcome
|
|
3
|
+
* record holds.
|
|
4
|
+
*
|
|
5
|
+
* A pure function: no model call, no clock, no I/O. The model's words only
|
|
6
|
+
* select which statement to check; the statement the kernel then makes is
|
|
7
|
+
* about the record, so it never claims more than the record establishes.
|
|
8
|
+
*
|
|
9
|
+
* It is built to stay silent. A notice that is wrong teaches people to ignore
|
|
10
|
+
* the line, so every doubt resolves to nothing: a negation, a hedge, a
|
|
11
|
+
* condition, a question, a quotation, code, a report of what someone else
|
|
12
|
+
* said, a statement about the past, a qualified claim ("all but one"), and any
|
|
13
|
+
* language but English. The claim wording and the words that cancel it are data
|
|
14
|
+
* below, each with the reason it is there.
|
|
15
|
+
*
|
|
16
|
+
* It looks at the record of THIS turn only. A turn whose record says nothing
|
|
17
|
+
* changed is never reported: "the tests pass" may be true of an earlier turn.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
AnswerClaimKind,
|
|
22
|
+
AnswerClaimMismatch,
|
|
23
|
+
AnswerClaimRecord,
|
|
24
|
+
TurnOutcome,
|
|
25
|
+
} from '../../../types/session/outcome.js'
|
|
26
|
+
import { auditCommandText } from './completion-audit.js'
|
|
27
|
+
|
|
28
|
+
/** Answers longer than this are read only up to it, so cost stays bounded. */
|
|
29
|
+
export const ANSWER_CLAIM_MAX_CHARS = 100_000
|
|
30
|
+
|
|
31
|
+
/** The longest quotation of the answer a mismatch carries, in characters. */
|
|
32
|
+
export const ANSWER_CLAIM_MAX_QUOTE_CHARS = 160
|
|
33
|
+
|
|
34
|
+
/** Words that may sit between a subject and its verb without changing the claim. */
|
|
35
|
+
const FILLER =
|
|
36
|
+
'(?:\\s+(?:all|now|also|still|again|both|are|were|is|have|has|been|currently|locally|fully))*'
|
|
37
|
+
|
|
38
|
+
/** Verbs that say a check came out well. */
|
|
39
|
+
const PASSED = '(?:pass(?:es|ed|ing)?|green|succeed(?:s|ed)?)\\b'
|
|
40
|
+
|
|
41
|
+
/** Determiners and kinds of test that may precede `tests`. */
|
|
42
|
+
const TEST_WORDS =
|
|
43
|
+
'(?:(?:all(?:\\s+of)?|the|my|our)\\s+)?(?:(?:unit|integration|e2e|existing|new|relevant|related|automated)\\s+)*'
|
|
44
|
+
|
|
45
|
+
interface ClaimPattern {
|
|
46
|
+
readonly kind: AnswerClaimKind
|
|
47
|
+
/** The claim in the kernel's words. */
|
|
48
|
+
readonly said: string
|
|
49
|
+
/** Matches the claim inside one clause. */
|
|
50
|
+
readonly re: RegExp
|
|
51
|
+
/** Why this wording, and why it is safe. */
|
|
52
|
+
readonly why: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Plain positive claims only. Each pattern needs the subject, at most a few
|
|
57
|
+
* filler words, and the verb, in that order, so a negation or a modal between
|
|
58
|
+
* them ("did not", "should") breaks the match by itself.
|
|
59
|
+
*/
|
|
60
|
+
export const ANSWER_CLAIM_PATTERNS: readonly ClaimPattern[] = [
|
|
61
|
+
{
|
|
62
|
+
kind: 'tests',
|
|
63
|
+
said: 'the tests pass',
|
|
64
|
+
re: new RegExp(`\\b${TEST_WORDS}tests\\b${FILLER}\\s+${PASSED}`, 'i'),
|
|
65
|
+
why: 'The plural "tests" as the subject. The singular is left out: "the test now passes" is usually about the one test just fixed, which says nothing about the run that failed.',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
kind: 'tests',
|
|
69
|
+
said: 'the test suite passes',
|
|
70
|
+
re: new RegExp(
|
|
71
|
+
`\\b(?:test\\s+suite|the\\s+suite|(?:whole|full|entire)\\s+(?:test\\s+)?suite|test\\s+run)\\b${FILLER}\\s+${PASSED}`,
|
|
72
|
+
'i',
|
|
73
|
+
),
|
|
74
|
+
why: 'The suite as a whole is the same claim as "the tests pass", also as "the whole suite". A bare "suite" is not enough: it also names a benchmark suite or a suite of rooms.',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
kind: 'tests',
|
|
78
|
+
said: 'no test failures',
|
|
79
|
+
re: /\b(?:no|zero|0)\s+(?:test\s+failures|failing\s+tests|failed\s+tests|test\s+errors)\b/i,
|
|
80
|
+
why: 'The same claim said by counting failures. It is matched whole, so the word "no" in it is not read as a negation.',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
kind: 'tests',
|
|
84
|
+
said: 'no test failures',
|
|
85
|
+
re: /\b(?:no\s+tests?\s+(?:are\s+|is\s+)?(?:fail(?:s|ing|ed)?)|tests?\s+(?:are\s+not|aren['\u2019]t|do\s+not|don['\u2019]t|did\s+not|didn['\u2019]t)\s+(?:fail|failing))\b/i,
|
|
86
|
+
why: 'The same claim said as a negated failure: "no tests are failing", "the tests do not fail". The pattern holds its own negation, and the failure word in it is not read as a failure.',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
kind: 'tests',
|
|
90
|
+
said: 'the tests pass',
|
|
91
|
+
re: /\b(?:ran|run|running)\s+(?:(?:all\s+)?(?:of\s+)?(?:the|my|our)\s+)?(?:\w+\s+){0,2}(?:tests|test\s+suite|suite)\s+and\s+(?:they|it)\s+(?:all\s+)?(?:pass(?:es|ed)?|are\s+(?:green|passing))\b/i,
|
|
92
|
+
why: 'The pronoun form, the commonest plain phrasing: "I ran the tests and they all pass". The run and its result are in one clause, and a negation between them ("they did not pass") breaks the match.',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
kind: 'checks',
|
|
96
|
+
said: 'the checks pass',
|
|
97
|
+
re: /^\W*(?:it['\u2019]?s\s+|it\s+is\s+|everything\s+is\s+)?all\s+green\b/i,
|
|
98
|
+
why: 'The shortest summary: "All green." Anchored to the start of the sentence, so "not all green" never matches.',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
kind: 'build',
|
|
102
|
+
said: 'the build succeeds',
|
|
103
|
+
re: /\b(?:it|everything)\s+compiles\b/i,
|
|
104
|
+
why: 'A bare "it compiles". The pronoun or "everything" is required: "the compiles list" and similar nouns are not claims.',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
kind: 'build',
|
|
108
|
+
said: 'the build succeeds',
|
|
109
|
+
re: new RegExp(
|
|
110
|
+
`\\b(?:build|compilation|compile)\\b${FILLER}\\s+(?:succeed(?:s|ed)?|pass(?:es|ed)?|green|completes?|completed|works|worked|is\\s+clean)\\b`,
|
|
111
|
+
'i',
|
|
112
|
+
),
|
|
113
|
+
why: 'The build as the subject with a success verb. "The build" alone, as in "the build script", never matches because the verb must follow.',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
kind: 'build',
|
|
117
|
+
said: 'the build succeeds',
|
|
118
|
+
re: /\b(?:builds?|compiles?)\s+(?:cleanly|successfully|fine)\b/i,
|
|
119
|
+
why: 'The adverb form: "it builds cleanly", "everything compiles successfully".',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
kind: 'lint',
|
|
123
|
+
said: 'lint is clean',
|
|
124
|
+
re: new RegExp(
|
|
125
|
+
`\\blint(?:ing|er)?\\b${FILLER}\\s+(?:is\\s+|are\\s+)?(?:clean|pass(?:es|ed|ing)?|green|succeed(?:s|ed)?|ok)\\b`,
|
|
126
|
+
'i',
|
|
127
|
+
),
|
|
128
|
+
why: 'Lint as the subject with a clean or passing verb.',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
kind: 'lint',
|
|
132
|
+
said: 'lint is clean',
|
|
133
|
+
re: /\bno\s+lint(?:ing)?\s+(?:errors?|warnings?|issues?|problems?)\b/i,
|
|
134
|
+
why: 'The same claim by counting: "no lint errors". Matched whole, so its own "no" is not a negation.',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
kind: 'typecheck',
|
|
138
|
+
said: 'type checking passes',
|
|
139
|
+
re: new RegExp(
|
|
140
|
+
`\\b(?:type[\\s-]?check(?:ing|s)?|typecheck|tsc)\\b${FILLER}\\s+(?:is\\s+|are\\s+)?(?:clean|pass(?:es|ed|ing)?|green|succeed(?:s|ed)?|ok)\\b`,
|
|
141
|
+
'i',
|
|
142
|
+
),
|
|
143
|
+
why: 'Type checking, the one-word spelling and the compiler named for it, as the subject. "Type checker" is left out: "the type checker passes the argument on" names what a checker does, not a result.',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
kind: 'typecheck',
|
|
147
|
+
said: 'type checking passes',
|
|
148
|
+
re: /\bno\s+type\s+errors\b/i,
|
|
149
|
+
why: 'The same claim by counting: "no type errors".',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
kind: 'checks',
|
|
153
|
+
said: 'the checks pass',
|
|
154
|
+
re: new RegExp(
|
|
155
|
+
`\\b(?:all\\s+(?:of\\s+)?(?:the\\s+)?checks|the\\s+(?:ci\\s+)?checks|ci\\s+checks|everything)\\b${FILLER}\\s+${PASSED}`,
|
|
156
|
+
'i',
|
|
157
|
+
),
|
|
158
|
+
why: 'The catch-all a summary reaches for: "all checks pass", "the CI checks pass", "everything is green".',
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
kind: 'tests',
|
|
162
|
+
said: 'the tests pass',
|
|
163
|
+
re: /^\W*(?:\*\*)?(?:tests?|test\s+suite)(?:\*\*)?\s*[:\-\u2013]\s*(?:\*\*)?\s*(?:all\s+)?(?:\d+\s+)?(?:pass(?:es|ed|ing)?|green|success(?:ful)?|ok|\u2705|\u2714)/i,
|
|
164
|
+
why: 'The label form of a summary list: "Tests: passing". Anchored to the start of the line, where a label stands.',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
kind: 'build',
|
|
168
|
+
said: 'the build succeeds',
|
|
169
|
+
re: /^\W*(?:\*\*)?build(?:\*\*)?\s*[:\-–]\s*(?:\*\*)?\s*(?:pass(?:es|ed|ing)?|green|success(?:ful)?|ok|clean|✅|✔)/i,
|
|
170
|
+
why: 'The label form: "Build: success".',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
kind: 'lint',
|
|
174
|
+
said: 'lint is clean',
|
|
175
|
+
re: /^\W*(?:\*\*)?lint(?:ing)?(?:\*\*)?\s*[:\-–]\s*(?:\*\*)?\s*(?:pass(?:es|ed|ing)?|green|success(?:ful)?|ok|clean|✅|✔)/i,
|
|
176
|
+
why: 'The label form: "Lint: clean".',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
kind: 'typecheck',
|
|
180
|
+
said: 'type checking passes',
|
|
181
|
+
re: /^\W*(?:\*\*)?(?:type[\s-]?check(?:ing)?|typecheck)(?:\*\*)?\s*[:\-–]\s*(?:\*\*)?\s*(?:pass(?:es|ed|ing)?|green|success(?:ful)?|ok|clean|✅|✔)/i,
|
|
182
|
+
why: 'The label form: "Typecheck: passing".',
|
|
183
|
+
},
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
/** Words that turn the claim around. Looked for in the few words before it. */
|
|
187
|
+
const NEGATION =
|
|
188
|
+
/\b(?:not|no|never|none|nothing|nobody|zero|scarcely|neither|nor|cannot|unable|without|barely|hardly|n't|isn|aren|doesn|don|didn|wasn|weren|won|couldn|can't|won't)\b|n['’]t\b/i
|
|
189
|
+
|
|
190
|
+
/** Words that make it a wish, a plan, a condition, a request or a guess. Looked for in the few words before it. */
|
|
191
|
+
const HEDGE =
|
|
192
|
+
/\b(?:if|once|when|whenever|unless|until|assuming|provided|whether|in\s+case|so\s+that|ensure|ensures|ensuring|make\s+sure|makes\s+sure|make|makes|get|gets|keep|keeps|verify|verifying|confirm|confirming|check\s+(?:that|whether)|hope|hopes|hoping|hopefully|expect|expects|expecting|expected|want|wants|need|needs|must|should|would|could|might|may|can|will|probably|likely|maybe|perhaps|presumably|supposedly|apparently|ideally|seems?|appears?|think|believe|assume|guess|suppose|imagine|wonder|let|try|please|goal|aim|aims|target)\b|'ll\b/i
|
|
193
|
+
|
|
194
|
+
/** Words that report what someone or something else said. Looked for in the whole clause. */
|
|
195
|
+
const REPORTING =
|
|
196
|
+
/\b(?:said|says|say|saying|claims?|claimed|claiming|reports?|reported|reporting|according\s+to|states?|stated|mentions?|mentioned|noted|told|tells|asked|asks|promises?|promised|advertises?|advertised)\b/i
|
|
197
|
+
|
|
198
|
+
/** Words that put the claim in the past, before this turn's change. Looked for in the whole clause. */
|
|
199
|
+
const EARLIER =
|
|
200
|
+
/\b(?:before|earlier|previously|prior|originally|beforehand|initially|yesterday|formerly|used\s+to|at\s+first|last\s+time|last\s+week|until\s+now|in\s+the\s+past|back\s+then|had\s+been)\b/i
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Words that make the claim partial, a guess or someone else's. Looked for in
|
|
204
|
+
* the few words after it. The last few are the plain negation of other languages
|
|
205
|
+
* ("değil", "nicht", "pas") for a clause that holds an English claim.
|
|
206
|
+
*/
|
|
207
|
+
const QUALIFIER =
|
|
208
|
+
/\b(?:except|besides|apart|aside|other\s+than|but\s+for|barring|mostly|partly|partially|mainly|almost|nearly|only|if|unless|provided|once|when|until|assuming|so\s+far\s+as|aside\s+from|on\s+ci|in\s+theory|apparently|supposedly|allegedly|presumably|seemingly|reportedly|hopefully|probably|maybe|perhaps|goal|aim|target|de[g\u011f]il|nicht|nein|pas)\b/i
|
|
209
|
+
|
|
210
|
+
/** A failure named anywhere else in the sentence: the sentence is reporting one, not claiming a pass. */
|
|
211
|
+
const FAILURE =
|
|
212
|
+
/\b(?:fail|fails|failed|failing|failure|failures|error|errors|broken|breaks|red|regress(?:ed|es|ion)?)\b/i
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* A word anywhere in the sentence that makes every claim in it a guess, or a
|
|
216
|
+
* claim in another language's negation. It sits at sentence level because a
|
|
217
|
+
* comma often separates it from the claim: "The tests pass, apparently."
|
|
218
|
+
*/
|
|
219
|
+
const SENTENCE_HEDGE =
|
|
220
|
+
/\b(?:apparently|supposedly|allegedly|presumably|seemingly|reportedly|hopefully|probably|maybe|perhaps|de[g\u011f]il|nicht|nein)\b/i
|
|
221
|
+
|
|
222
|
+
/** Words a sentence may start with that make everything after it conditional. */
|
|
223
|
+
const CONDITIONAL_START =
|
|
224
|
+
/^\W*(?:if|once|when|whenever|unless|assuming|provided|in\s+case|should|were|had)\b/i
|
|
225
|
+
|
|
226
|
+
/** How many words before and after a claim are read for what cancels it. */
|
|
227
|
+
const WORDS_BEFORE = 6
|
|
228
|
+
const WORDS_AFTER = 6
|
|
229
|
+
|
|
230
|
+
/** What a verb that takes an object starts with: "the type checker passes the argument on" is not a result. */
|
|
231
|
+
const OBJECT_AFTER =
|
|
232
|
+
/^\W*(?:the|a|an|its|their|his|her|them|it|this|that|these|those|each|every|any|some|through|along)\b/i
|
|
233
|
+
|
|
234
|
+
/** A claim worded as a subject ("tests passing is the goal"): the verb is a gerund that a copula follows. */
|
|
235
|
+
const COPULA_AFTER = /^\W*(?:is|are|was|were|means|requires|needs)\b/i
|
|
236
|
+
|
|
237
|
+
/** What is left of an answer once the places a claim does not count are taken out. */
|
|
238
|
+
function readableText(answer: string): string {
|
|
239
|
+
return (
|
|
240
|
+
answer
|
|
241
|
+
.slice(0, ANSWER_CLAIM_MAX_CHARS)
|
|
242
|
+
// A fenced block, or one the answer never closed.
|
|
243
|
+
.replace(/(```|~~~)[\s\S]*?(?:\1|$)/g, ' _ ')
|
|
244
|
+
// A block quotation: its lines are somebody else's words.
|
|
245
|
+
.replace(/^[ \t]*>.*$/gm, ' ')
|
|
246
|
+
// An inline code span names a command or a symbol, not a claim.
|
|
247
|
+
.replace(/`[^`\n]{0,400}`/g, ' _ ')
|
|
248
|
+
// A quotation in either kind of double quotes.
|
|
249
|
+
.replace(/"[^"\n]{0,400}"/g, ' _ ')
|
|
250
|
+
.replace(/“[^”\n]{0,400}”/g, ' _ ')
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Sentences, and within one the clauses a claim and its hedge do not cross. */
|
|
255
|
+
function clausesOf(text: string): { sentenceEndsWithQuestion: boolean; clauses: string[] }[] {
|
|
256
|
+
const out: { sentenceEndsWithQuestion: boolean; clauses: string[] }[] = []
|
|
257
|
+
// A semicolon ends a statement, so a failure named in one half does not cancel a claim in the other.
|
|
258
|
+
for (const raw of text.split(/(?<=[.!?])\s+|\n+|;\s*/)) {
|
|
259
|
+
const sentence = raw.trim()
|
|
260
|
+
if (sentence === '') continue
|
|
261
|
+
if (CONDITIONAL_START.test(sentence) || SENTENCE_HEDGE.test(sentence)) continue
|
|
262
|
+
// What the claim patterns themselves match ("no test failures") is not a failure.
|
|
263
|
+
let rest = sentence
|
|
264
|
+
for (const pattern of ANSWER_CLAIM_PATTERNS) {
|
|
265
|
+
rest = rest.replace(new RegExp(pattern.re.source, 'gi'), ' ')
|
|
266
|
+
}
|
|
267
|
+
// "no errors", "without failures": a count of none is not a failure.
|
|
268
|
+
rest = rest.replace(/\b(?:no|zero|0|without)\s+(?:\w+\s+)?(?:errors?|failures?)\b/gi, ' ')
|
|
269
|
+
if (FAILURE.test(rest)) continue
|
|
270
|
+
out.push({
|
|
271
|
+
sentenceEndsWithQuestion: /\?\W*$/.test(sentence),
|
|
272
|
+
// A comma, a semicolon, a dash, a bracket, or a word that turns the sentence around.
|
|
273
|
+
clauses: sentence.split(
|
|
274
|
+
/[,;()—]|\s-\s|\s–\s|\b(?:but|however|although|though|while|whereas|yet|because|since|so)\b/i,
|
|
275
|
+
),
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
return out
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function wordsBefore(clause: string, at: number): string {
|
|
282
|
+
return clause.slice(0, at).trim().split(/\s+/).slice(-WORDS_BEFORE).join(' ')
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function wordsAfter(clause: string, to: number): string {
|
|
286
|
+
return clause.slice(to).trim().split(/\s+/).slice(0, WORDS_AFTER).join(' ')
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** The claim in this clause, when it is a plain, present, positive one. */
|
|
290
|
+
function claimIn(clause: string): ClaimPattern | undefined {
|
|
291
|
+
if (REPORTING.test(clause) || EARLIER.test(clause)) return undefined
|
|
292
|
+
for (const pattern of ANSWER_CLAIM_PATTERNS) {
|
|
293
|
+
const found = pattern.re.exec(clause)
|
|
294
|
+
if (!found) continue
|
|
295
|
+
const before = wordsBefore(clause, found.index)
|
|
296
|
+
if (NEGATION.test(before) || HEDGE.test(before)) continue
|
|
297
|
+
const after = wordsAfter(clause, found.index + found[0].length)
|
|
298
|
+
if (QUALIFIER.test(after)) continue
|
|
299
|
+
if (/\bpass(?:es)?$/i.test(found[0]) && OBJECT_AFTER.test(after)) continue
|
|
300
|
+
if (/\bpassing$/i.test(found[0]) && COPULA_AFTER.test(after)) continue
|
|
301
|
+
if (/\d+\s+of\s+\d+/.test(clause)) continue
|
|
302
|
+
return pattern
|
|
303
|
+
}
|
|
304
|
+
return undefined
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** The kind of check a command line is, when it says so. */
|
|
308
|
+
const COMMAND_KINDS: readonly { kind: AnswerClaimKind; re: RegExp }[] = [
|
|
309
|
+
{
|
|
310
|
+
kind: 'lint',
|
|
311
|
+
re: /\b(?:lint|eslint|biome|prettier|ruff|flake8|rubocop|clippy|golangci-lint)\b/i,
|
|
312
|
+
},
|
|
313
|
+
{ kind: 'typecheck', re: /\b(?:typecheck|tsc|mypy|pyright)\b/i },
|
|
314
|
+
{ kind: 'tests', re: /\b(?:test|tests|vitest|jest|pytest|mocha|phpunit|spec)\b/i },
|
|
315
|
+
{ kind: 'build', re: /\b(?:build|compile|bundle)\b/i },
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
/** A type check is a compile, so a claim about either is about both. */
|
|
319
|
+
function sameKind(a: AnswerClaimKind, b: AnswerClaimKind): boolean {
|
|
320
|
+
if (a === b) return true
|
|
321
|
+
return (a === 'build' && b === 'typecheck') || (a === 'typecheck' && b === 'build')
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Every kind a command line is: a chained `pnpm lint && pnpm test` is both. */
|
|
325
|
+
function kindsOfCommand(command: string): AnswerClaimKind[] {
|
|
326
|
+
return COMMAND_KINDS.filter((k) => k.re.test(command)).map((k) => k.kind)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** The failed check the claim is about, or `undefined` when every failed check is plainly another kind. */
|
|
330
|
+
function failedCheckFor(
|
|
331
|
+
record: TurnOutcome,
|
|
332
|
+
claim: AnswerClaimKind,
|
|
333
|
+
): { command?: string } | undefined {
|
|
334
|
+
const checks = Array.isArray(record.checks) ? record.checks : []
|
|
335
|
+
const failed = checks.filter((c) => c?.result === 'failed')
|
|
336
|
+
if (failed.length === 0) return {}
|
|
337
|
+
const related = failed.filter((c) => {
|
|
338
|
+
const kinds = kindsOfCommand(typeof c.command === 'string' ? c.command : '')
|
|
339
|
+
return claim === 'checks' || kinds.length === 0 || kinds.some((kind) => sameKind(kind, claim))
|
|
340
|
+
})
|
|
341
|
+
if (related.length === 0) return undefined
|
|
342
|
+
const only = related.length === 1 ? related[0]?.command : undefined
|
|
343
|
+
return typeof only === 'string' ? { command: only } : {}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* The one mismatch between a plain claim in `answer` and this turn's `outcome`,
|
|
348
|
+
* or `undefined`.
|
|
349
|
+
*
|
|
350
|
+
* Reports only when the record says a check failed, or says files changed and
|
|
351
|
+
* no check ran or none established a pass. Never when the record says nothing
|
|
352
|
+
* changed, that the checks passed, or that it cannot tell: in those the claim
|
|
353
|
+
* may be true. English only: a claim in another language is not found.
|
|
354
|
+
*/
|
|
355
|
+
export function compareAnswerToOutcome(
|
|
356
|
+
answer: string | undefined | null,
|
|
357
|
+
outcome: TurnOutcome | undefined,
|
|
358
|
+
): AnswerClaimMismatch | undefined {
|
|
359
|
+
// A notice is never worth failing a turn for: input that does not have the
|
|
360
|
+
// expected shape (a hook-supplied record) is read as "nothing to say".
|
|
361
|
+
try {
|
|
362
|
+
return compare(answer, outcome)
|
|
363
|
+
} catch {
|
|
364
|
+
return undefined
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function compare(
|
|
369
|
+
answer: string | undefined | null,
|
|
370
|
+
outcome: TurnOutcome | undefined,
|
|
371
|
+
): AnswerClaimMismatch | undefined {
|
|
372
|
+
if (typeof answer !== 'string' || answer === '' || !outcome) return undefined
|
|
373
|
+
const verdict = outcome.verdict
|
|
374
|
+
if (verdict !== 'failing' && verdict !== 'unchecked' && verdict !== 'inconclusive') {
|
|
375
|
+
return undefined
|
|
376
|
+
}
|
|
377
|
+
for (const { sentenceEndsWithQuestion, clauses } of clausesOf(readableText(answer))) {
|
|
378
|
+
if (sentenceEndsWithQuestion) continue
|
|
379
|
+
for (const clause of clauses) {
|
|
380
|
+
const pattern = claimIn(clause)
|
|
381
|
+
if (!pattern) continue
|
|
382
|
+
let record: AnswerClaimRecord
|
|
383
|
+
let recordSays: string
|
|
384
|
+
if (verdict === 'failing') {
|
|
385
|
+
const failed = failedCheckFor(outcome, pattern.kind)
|
|
386
|
+
if (!failed) continue
|
|
387
|
+
record = 'check-failed'
|
|
388
|
+
recordSays = failed.command
|
|
389
|
+
? `${auditCommandText(failed.command)} failed`
|
|
390
|
+
: 'a check failed'
|
|
391
|
+
} else if (verdict === 'unchecked') {
|
|
392
|
+
record = 'unchecked'
|
|
393
|
+
recordSays = 'files changed and no check ran'
|
|
394
|
+
} else {
|
|
395
|
+
record = 'unreadable'
|
|
396
|
+
recordSays = 'files changed and no check gave a result that could be read'
|
|
397
|
+
}
|
|
398
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: control characters are what this removes
|
|
399
|
+
const folded = clause.replace(/[\u0000-\u001f\u007f-\u009f\s]+/g, ' ').trim()
|
|
400
|
+
const quote =
|
|
401
|
+
folded.length <= ANSWER_CLAIM_MAX_QUOTE_CHARS
|
|
402
|
+
? folded
|
|
403
|
+
: `${folded.slice(0, ANSWER_CLAIM_MAX_QUOTE_CHARS - 3)}...`
|
|
404
|
+
return { claim: pattern.kind, said: pattern.said, quote, record, recordSays }
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return undefined
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** The mismatch as one plain line, for a person: `The answer says the tests pass. The record: ...` */
|
|
411
|
+
export function describeAnswerClaimMismatch(mismatch: AnswerClaimMismatch): string {
|
|
412
|
+
return `The answer says ${mismatch.said}. The record: ${mismatch.recordSays}.`
|
|
413
|
+
}
|
|
@@ -539,6 +539,18 @@ export class OutcomeLedger {
|
|
|
539
539
|
return this.compute(null)
|
|
540
540
|
}
|
|
541
541
|
|
|
542
|
+
/**
|
|
543
|
+
* The record as it stands now, with the workspace asked once, and without
|
|
544
|
+
* settling: the turn may go on and `finish` still answers once at the end.
|
|
545
|
+
* Used by a host that wants the whole record before it lets a turn end. A
|
|
546
|
+
* record that is not whole is `undefined`, as in `finish`.
|
|
547
|
+
*/
|
|
548
|
+
async preview(): Promise<TurnOutcome | undefined> {
|
|
549
|
+
if (this.unrecorded) return undefined
|
|
550
|
+
if (this.settled !== undefined) return this.settled
|
|
551
|
+
return this.compute(this.started ? await this.detect() : null)
|
|
552
|
+
}
|
|
553
|
+
|
|
542
554
|
private compute(endFingerprint: string | null): TurnOutcome {
|
|
543
555
|
const workspace = this.started && this.startFingerprint !== null && endFingerprint !== null
|
|
544
556
|
const moved = workspace && this.startFingerprint !== endFingerprint
|
package/src/store/goal/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from 'node:path'
|
|
|
3
3
|
import { TenantIsolationError } from '../../session/errors.js'
|
|
4
4
|
import type {
|
|
5
5
|
GoalBlockReason,
|
|
6
|
+
GoalConfirmedBy,
|
|
6
7
|
GoalPhase,
|
|
7
8
|
GoalRef,
|
|
8
9
|
GoalRoundAuthority,
|
|
@@ -29,6 +30,8 @@ export interface CreateSessionGoalParams {
|
|
|
29
30
|
readonly sessionId: SessionId
|
|
30
31
|
readonly objective: string
|
|
31
32
|
readonly maxGoalRounds?: number
|
|
33
|
+
/** Set only when this goal chooses; absent leaves the choice to the host's setting. */
|
|
34
|
+
readonly confirmedBy?: GoalConfirmedBy
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
export interface EditSessionGoalParams {
|
|
@@ -273,6 +276,13 @@ abstract class SessionGoalStoreBase implements SessionGoalStore {
|
|
|
273
276
|
const sessionId = await this.requireSession(params.sessionId, tenantId)
|
|
274
277
|
const text = objective(params.objective)
|
|
275
278
|
const maxRounds = maxGoalRounds(params.maxGoalRounds)
|
|
279
|
+
if (
|
|
280
|
+
params.confirmedBy !== undefined &&
|
|
281
|
+
params.confirmedBy !== 'model' &&
|
|
282
|
+
params.confirmedBy !== 'record'
|
|
283
|
+
) {
|
|
284
|
+
throw new TypeError('goal confirmedBy must be "model" or "record".')
|
|
285
|
+
}
|
|
276
286
|
return await backendFor(this).transact(sessionId, (existing) => {
|
|
277
287
|
if (existing) {
|
|
278
288
|
assertTenant(existing, tenantId)
|
|
@@ -290,6 +300,7 @@ abstract class SessionGoalStoreBase implements SessionGoalStore {
|
|
|
290
300
|
phase: 'active',
|
|
291
301
|
maxGoalRounds: maxRounds,
|
|
292
302
|
roundsAdmitted: 0,
|
|
303
|
+
...(params.confirmedBy !== undefined ? { confirmedBy: params.confirmedBy } : {}),
|
|
293
304
|
createdAt: now,
|
|
294
305
|
updatedAt: now,
|
|
295
306
|
}
|
|
@@ -57,3 +57,32 @@ export function getActiveSpanContext(): SpanContext | undefined {
|
|
|
57
57
|
return undefined
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The active span as the W3C trace context an outgoing request can carry:
|
|
63
|
+
* `{ traceparent, tracestate? }`, or `{}` when no span is active.
|
|
64
|
+
*
|
|
65
|
+
* `{}` is what a host that never registered telemetry gets, for the same
|
|
66
|
+
* reason {@link getActiveSpanContext} returns `undefined` there: nothing is
|
|
67
|
+
* special-cased, the api's default context has no span. A span context that
|
|
68
|
+
* is not valid W3C (an all-zero id, a wrong length) also yields `{}`, since a
|
|
69
|
+
* malformed `traceparent` is worse than none.
|
|
70
|
+
*/
|
|
71
|
+
export function getTraceContextHeaders(): { traceparent?: string; tracestate?: string } {
|
|
72
|
+
const span = getActiveSpanContext()
|
|
73
|
+
if (!span) return {}
|
|
74
|
+
const { traceId, spanId } = span
|
|
75
|
+
if (!/^[0-9a-f]{32}$/.test(traceId) || /^0+$/.test(traceId)) return {}
|
|
76
|
+
if (!/^[0-9a-f]{16}$/.test(spanId) || /^0+$/.test(spanId)) return {}
|
|
77
|
+
const flags = (span.traceFlags & 0xff).toString(16).padStart(2, '0')
|
|
78
|
+
let tracestate: string | undefined
|
|
79
|
+
try {
|
|
80
|
+
tracestate = span.traceState?.serialize() || undefined
|
|
81
|
+
} catch {
|
|
82
|
+
tracestate = undefined
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
traceparent: `00-${traceId}-${spanId}-${flags}`,
|
|
86
|
+
...(tracestate ? { tracestate } : {}),
|
|
87
|
+
}
|
|
88
|
+
}
|