@namzu/sdk 56.1.0 → 56.2.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 +17 -0
- package/dist/compaction/context-breakdown.d.ts +1 -1
- package/dist/compaction/context-breakdown.d.ts.map +1 -1
- package/dist/compaction/context-breakdown.js +9 -2
- package/dist/compaction/context-breakdown.js.map +1 -1
- package/dist/compaction/verifier.d.ts.map +1 -1
- package/dist/compaction/verifier.js +3 -1
- package/dist/compaction/verifier.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +1 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +8 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +10 -2
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/file-evidence-context.d.ts +21 -4
- package/dist/runtime/query/file-evidence-context.d.ts.map +1 -1
- package/dist/runtime/query/file-evidence-context.js +70 -8
- package/dist/runtime/query/file-evidence-context.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +2 -11
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +1 -2
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +30 -19
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/recorded-context.d.ts +18 -0
- package/dist/runtime/query/iteration/recorded-context.d.ts.map +1 -0
- package/dist/runtime/query/iteration/recorded-context.js +37 -0
- package/dist/runtime/query/iteration/recorded-context.js.map +1 -0
- package/dist/runtime/query/iteration/step-context.d.ts +15 -51
- package/dist/runtime/query/iteration/step-context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/step-context.js +24 -134
- package/dist/runtime/query/iteration/step-context.js.map +1 -1
- package/dist/runtime/query/iteration/step-shaping.d.ts +20 -8
- package/dist/runtime/query/iteration/step-shaping.d.ts.map +1 -1
- package/dist/runtime/query/iteration/step-shaping.js +52 -26
- package/dist/runtime/query/iteration/step-shaping.js.map +1 -1
- package/dist/scheduler/completion-inbox.d.ts +27 -0
- package/dist/scheduler/completion-inbox.d.ts.map +1 -1
- package/dist/scheduler/completion-inbox.js +88 -21
- package/dist/scheduler/completion-inbox.js.map +1 -1
- package/dist/store/session-index/fts.d.ts.map +1 -1
- package/dist/store/session-index/fts.js +6 -0
- package/dist/store/session-index/fts.js.map +1 -1
- package/dist/test-support/file-evidence-held.d.ts +11 -0
- package/dist/test-support/file-evidence-held.d.ts.map +1 -0
- package/dist/test-support/file-evidence-held.js +28 -0
- package/dist/test-support/file-evidence-held.js.map +1 -0
- package/dist/test-support/request-only.d.ts +10 -0
- package/dist/test-support/request-only.d.ts.map +1 -0
- package/dist/test-support/request-only.js +15 -0
- package/dist/test-support/request-only.js.map +1 -0
- package/dist/tools/untrusted-envelope.d.ts +7 -0
- package/dist/tools/untrusted-envelope.d.ts.map +1 -1
- package/dist/tools/untrusted-envelope.js +3 -1
- package/dist/tools/untrusted-envelope.js.map +1 -1
- package/dist/turn/evidence-recall.d.ts +19 -8
- package/dist/turn/evidence-recall.d.ts.map +1 -1
- package/dist/turn/evidence-recall.js +134 -53
- package/dist/turn/evidence-recall.js.map +1 -1
- package/dist/turn/memory-recall.d.ts +18 -0
- package/dist/turn/memory-recall.d.ts.map +1 -1
- package/dist/turn/memory-recall.js +43 -4
- package/dist/turn/memory-recall.js.map +1 -1
- package/dist/types/message/index.d.ts +46 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +78 -1
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/session/prepare-step.d.ts +24 -0
- package/dist/types/session/prepare-step.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/compaction/context-breakdown.ts +11 -3
- package/src/compaction/verifier.ts +3 -1
- package/src/public-runtime.ts +7 -0
- package/src/runtime/query/executor.ts +20 -2
- package/src/runtime/query/file-evidence-context.ts +79 -11
- package/src/runtime/query/index.ts +2 -10
- package/src/runtime/query/iteration/index.ts +32 -26
- package/src/runtime/query/iteration/recorded-context.ts +48 -0
- package/src/runtime/query/iteration/step-context.ts +27 -155
- package/src/runtime/query/iteration/step-shaping.ts +64 -40
- package/src/scheduler/completion-inbox.ts +87 -20
- package/src/store/session-index/fts.ts +5 -0
- package/src/test-support/file-evidence-held.ts +27 -0
- package/src/test-support/request-only.ts +19 -0
- package/src/tools/untrusted-envelope.ts +10 -1
- package/src/turn/evidence-recall.ts +189 -58
- package/src/turn/memory-recall.ts +59 -5
- package/src/types/message/index.ts +108 -1
- package/src/types/session/prepare-step.ts +26 -0
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
import { getTracer } from '../../../telemetry/runtime-accessors.js'
|
|
29
29
|
import { STRUCTURED_OUTPUT_TOOL_NAME } from '../../../tools/builtins/structuredOutput.js'
|
|
30
30
|
import type { ToolManager } from '../../../toolsets/manager.js'
|
|
31
|
-
import { splitAtEvidenceBlocks } from '../../../turn/evidence-recall.js'
|
|
32
31
|
import type { CostInfo, TokenUsage } from '../../../types/common/index.js'
|
|
33
32
|
import { NamzuError } from '../../../types/errors/index.js'
|
|
34
33
|
import type { MessageId, SessionId, TurnId } from '../../../types/ids/index.js'
|
|
@@ -58,7 +57,6 @@ import { createCallbackInference } from '../callback-inference.js'
|
|
|
58
57
|
import { SandboxAcquisitionStopped } from '../deferred-sandbox.js'
|
|
59
58
|
import { deliverDurableInbound } from '../durable-inbound.js'
|
|
60
59
|
import type { ToolCallOutcome } from '../executor.js'
|
|
61
|
-
import { FILE_EVIDENCE_NONE } from '../file-evidence-context.js'
|
|
62
60
|
import { projectObservationContext } from '../observation-context.js'
|
|
63
61
|
import { applyLifecycleHookResults } from '../plugin-hooks.js'
|
|
64
62
|
import {
|
|
@@ -103,17 +101,16 @@ import {
|
|
|
103
101
|
type TextOnlyModelRefusal,
|
|
104
102
|
streamWithProviderRejectedImageRecovery,
|
|
105
103
|
} from './provider-rejected-image.js'
|
|
104
|
+
import { appendRecordedContext } from './recorded-context.js'
|
|
106
105
|
import { type MediaRemoval, RequestRepair } from './request-repair.js'
|
|
107
|
-
import {
|
|
106
|
+
import { placeRequestContext } from './step-context.js'
|
|
108
107
|
import {
|
|
109
|
-
FILE_EVIDENCE_KEY,
|
|
110
108
|
type StepShaping,
|
|
111
|
-
appendWorkContext,
|
|
112
109
|
beforeStep,
|
|
113
110
|
prepareStep,
|
|
114
111
|
selectContextModel,
|
|
115
112
|
stepContextMessage,
|
|
116
|
-
|
|
113
|
+
workContext,
|
|
117
114
|
} from './step-shaping.js'
|
|
118
115
|
import { streamProviderTurn } from './stream-turn.js'
|
|
119
116
|
|
|
@@ -194,8 +191,7 @@ export class IterationOrchestrator {
|
|
|
194
191
|
* one process must not suppress each other's first envelope.
|
|
195
192
|
*/
|
|
196
193
|
private lastEnvelopeKey: string | undefined
|
|
197
|
-
/**
|
|
198
|
-
private readonly turnContext = new TurnContextCarry()
|
|
194
|
+
/** The previous request's context, so a pre-call hook is told what changed. */
|
|
199
195
|
private previousRequestContext: RequestContextSnapshot | undefined
|
|
200
196
|
|
|
201
197
|
private projectObservations(messages: Message[]): Message[] {
|
|
@@ -838,6 +834,11 @@ export class IterationOrchestrator {
|
|
|
838
834
|
'limit-finalization',
|
|
839
835
|
)
|
|
840
836
|
: undefined
|
|
837
|
+
// What the step asked to have kept in the conversation joins the
|
|
838
|
+
// history now, after its last message, so this request and every
|
|
839
|
+
// later one begin with it. Nothing is inserted ahead of a message
|
|
840
|
+
// that is already recorded.
|
|
841
|
+
appendRecordedContext(recorder, step.recordedContext, this.ctx.log)
|
|
841
842
|
const baseMessages = recorder.messages
|
|
842
843
|
|
|
843
844
|
// Step guidance is appended to the REQUEST, never pushed onto
|
|
@@ -911,24 +912,30 @@ export class IterationOrchestrator {
|
|
|
911
912
|
DEFAULT_MAX_REQUEST_RICH_CONTENT_BYTES,
|
|
912
913
|
{ imagesReadable: this.modelReadsImages(stepModel) },
|
|
913
914
|
)
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
//
|
|
918
|
-
//
|
|
919
|
-
//
|
|
920
|
-
// request begins with the whole of the one before it. The short
|
|
921
|
-
// reminders and notices stay after the history.
|
|
915
|
+
// Request-only data (the context contributions, notes about the
|
|
916
|
+
// search, the work context) goes last on every call, after the
|
|
917
|
+
// whole history, so the history a caching driver reads never
|
|
918
|
+
// changes shape from one call to the next. What a step wants
|
|
919
|
+
// kept is recorded in the history above instead. The short
|
|
920
|
+
// reminders and notices follow it.
|
|
922
921
|
const messages = [...projected]
|
|
923
|
-
|
|
922
|
+
// What the loop learns about files and finished work holds until it
|
|
923
|
+
// changes, so it is recorded, after the history as it stands, and
|
|
924
|
+
// carried in this request where it will sit in the next one's history.
|
|
925
|
+
const work = workContext(shaping, projected, iterationNum, step)
|
|
926
|
+
const recordedNow = appendRecordedContext(recorder, work.recorded, this.ctx.log)
|
|
927
|
+
placeRequestContext(
|
|
924
928
|
messages,
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
929
|
+
recordedNow,
|
|
930
|
+
// The working-memory block and the policy notice already end the
|
|
931
|
+
// request; the entries just recorded belong ahead of them.
|
|
932
|
+
(workingMemory.context ? 1 : 0) + (policyNotice ? 1 : 0),
|
|
933
|
+
[
|
|
934
|
+
...[contextText, step.context]
|
|
935
|
+
.filter((text): text is string => Boolean(text))
|
|
936
|
+
.map(stepContextMessage),
|
|
937
|
+
...work.requestOnly,
|
|
938
|
+
],
|
|
932
939
|
)
|
|
933
940
|
if (closingDirective) messages.push(closingDirective)
|
|
934
941
|
await this.reportUnsupportedToolResults(messages)
|
|
@@ -2896,9 +2903,8 @@ export class IterationOrchestrator {
|
|
|
2896
2903
|
this.ctx.turnConfig.maxRequestRichContentBytes ?? DEFAULT_MAX_REQUEST_RICH_CONTENT_BYTES,
|
|
2897
2904
|
{ imagesReadable: this.modelReadsImages(model) },
|
|
2898
2905
|
)
|
|
2899
|
-
appendWorkContext(this.stepShaping(), finalMessages, this.steps.length + 1, { model })
|
|
2900
2906
|
// The closing directive is the last thing the model reads: after
|
|
2901
|
-
// the working memory
|
|
2907
|
+
// the working memory above.
|
|
2902
2908
|
finalMessages.push(
|
|
2903
2909
|
createRuntimeContextMessage(
|
|
2904
2910
|
`[SYSTEM] Turn is ending due to ${reason}. ${CLOSING_RESPONSE_GUIDANCE}`,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { NAMZU } from '../../../constants/telemetry/index.js'
|
|
2
|
+
import type { TurnRecorder } from '../../../manager/session/turn-recorder.js'
|
|
3
|
+
import {
|
|
4
|
+
type Message,
|
|
5
|
+
createTurnContextEntry,
|
|
6
|
+
isTurnContextKey,
|
|
7
|
+
turnContextEntries,
|
|
8
|
+
} from '../../../types/message/index.js'
|
|
9
|
+
import type { RecordedContextItem } from '../../../types/session/index.js'
|
|
10
|
+
import type { Logger } from '../../../utils/logger.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Append what a step asked to have recorded, after whatever the history
|
|
14
|
+
* ends with.
|
|
15
|
+
*
|
|
16
|
+
* Only at the end: an entry slipped in ahead of the operator's message would
|
|
17
|
+
* put the live array out of log order, and the recorder would then write a
|
|
18
|
+
* whole compaction record to re-base it on every turn. An item adds nothing
|
|
19
|
+
* when its text is empty or equals the last entry under its key, so a step
|
|
20
|
+
* that offers the same text on every call records it once. Returns the
|
|
21
|
+
* entries it added, so a request built before they were added can carry them
|
|
22
|
+
* at the place they will hold.
|
|
23
|
+
*/
|
|
24
|
+
export function appendRecordedContext(
|
|
25
|
+
recorder: Pick<TurnRecorder, 'messages' | 'pushMessage' | 'turnId'>,
|
|
26
|
+
items: readonly RecordedContextItem[] | undefined,
|
|
27
|
+
log: Pick<Logger, 'warn'>,
|
|
28
|
+
at: Date = new Date(),
|
|
29
|
+
): Message[] {
|
|
30
|
+
const appended: Message[] = []
|
|
31
|
+
for (const item of items ?? []) {
|
|
32
|
+
const text = item.text.trim()
|
|
33
|
+
if (text.length === 0) continue
|
|
34
|
+
if (!isTurnContextKey(item.key)) {
|
|
35
|
+
log.warn('A step recorded context under a key that is not a name; it was skipped', {
|
|
36
|
+
[NAMZU.TURN_ID]: recorder.turnId,
|
|
37
|
+
'namzu.runtime.key': String(item.key).slice(0, 80),
|
|
38
|
+
})
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
const earlier = turnContextEntries(recorder.messages, item.key)
|
|
42
|
+
if (earlier.at(-1) === text) continue
|
|
43
|
+
const entry = createTurnContextEntry(item.key, text, at, { later: earlier.length > 0 })
|
|
44
|
+
recorder.pushMessage(entry)
|
|
45
|
+
appended.push(entry)
|
|
46
|
+
}
|
|
47
|
+
return appended
|
|
48
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createRuntimeContextMessage } from '../../../types/message/index.js'
|
|
2
2
|
import type { Message } from '../../../types/message/index.js'
|
|
3
|
-
import { isOperatorUserMessage } from '../steering.js'
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Request-only context, as the model reads it: a runtime-context user
|
|
@@ -18,167 +17,40 @@ export function stepContextMessage(content: string) {
|
|
|
18
17
|
)
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
/**
|
|
22
|
-
* Put `blocks` in front of the operator's new message when that message is
|
|
23
|
-
* what the model is about to answer, so the request is the last thing it
|
|
24
|
-
* reads and the longest data comes before it. Only the reminders and other
|
|
25
|
-
* request-only context may follow that message: once the turn has an
|
|
26
|
-
* assistant or tool message after it, the blocks stay at the end of the
|
|
27
|
-
* history, where a caching driver ends its breakpoint before them, so the
|
|
28
|
-
* tool loop of a long turn is not re-read at full price on every step.
|
|
29
|
-
*
|
|
30
|
-
* Reminders and notices that follow the operator's message stay where they
|
|
31
|
-
* are: they are short, and the edge after the request is where they are read.
|
|
32
|
-
*/
|
|
33
|
-
export function placeBeforeLatestOperatorMessage(history: Message[], blocks: readonly Message[]) {
|
|
34
|
-
if (blocks.length === 0) return
|
|
35
|
-
let at = history.length
|
|
36
|
-
for (let i = history.length - 1; i >= 0; i--) {
|
|
37
|
-
const message = history[i] as Message
|
|
38
|
-
if (isOperatorUserMessage(message)) {
|
|
39
|
-
at = i
|
|
40
|
-
break
|
|
41
|
-
}
|
|
42
|
-
// A reply or a tool result after the message: the model has moved on.
|
|
43
|
-
if (message.role === 'assistant' || message.role === 'tool') break
|
|
44
|
-
}
|
|
45
|
-
history.splice(at, 0, ...blocks)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface PlacedBlock {
|
|
49
|
-
readonly text: string
|
|
50
|
-
/** The operator message this block precedes, or the history message it follows. */
|
|
51
|
-
at: { readonly before: Message } | { readonly after: Message | undefined }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
20
|
function textOf(message: Message): string {
|
|
55
21
|
return typeof message.content === 'string' ? message.content : JSON.stringify(message.content)
|
|
56
22
|
}
|
|
57
23
|
|
|
58
|
-
/** The operator message the model is about to answer, when there is one. */
|
|
59
|
-
function pendingOperatorMessage(history: readonly Message[]): Message | undefined {
|
|
60
|
-
for (let i = history.length - 1; i >= 0; i--) {
|
|
61
|
-
const message = history[i] as Message
|
|
62
|
-
if (isOperatorUserMessage(message)) return message
|
|
63
|
-
if (message.role === 'assistant' || message.role === 'tool') return undefined
|
|
64
|
-
}
|
|
65
|
-
return undefined
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** A block whose own text says that a later one replaces it, under the name of what it reports. */
|
|
69
|
-
export interface ReplaceableBlock {
|
|
70
|
-
readonly key: string
|
|
71
|
-
readonly message: Message
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** How many superseded versions of a replaceable block still go with the latest. */
|
|
75
|
-
export const EARLIER_VERSIONS_KEPT = 2
|
|
76
|
-
|
|
77
|
-
interface VersionedBlock extends PlacedBlock {
|
|
78
|
-
readonly message: Message
|
|
79
|
-
}
|
|
80
|
-
|
|
81
24
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* What a block says is the step's own business: a block it offers again with
|
|
85
|
-
* the same text is the same block, and it keeps the place it first had; a
|
|
86
|
-
* block it no longer offers is withdrawn, so corrected data never lingers.
|
|
87
|
-
* A block seen for the first time goes before the operator's message when that
|
|
88
|
-
* message is what the model is answering, and otherwise after the history of
|
|
89
|
-
* that moment. So the requests of a turn share their opening up to the first
|
|
90
|
-
* block that changed, and a caching driver keeps reading the same prefix
|
|
91
|
-
* instead of re-reading the turn from the operator's message on.
|
|
25
|
+
* Put a request's own data into it: after everything the history holds, in
|
|
26
|
+
* the order offered, each distinct text once.
|
|
92
27
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
28
|
+
* Request-only data is what the model needs for this request alone (a note
|
|
29
|
+
* that something is unavailable, the work still running). It is re-placed on
|
|
30
|
+
* every call and always last, so the history before it never changes shape
|
|
31
|
+
* from one call to the next and a caching driver, which ends its breakpoint
|
|
32
|
+
* before the first such block, covers the whole history on every call. What
|
|
33
|
+
* the model should keep reading in later requests is recorded as turn context
|
|
34
|
+
* instead (`PrepareStepResult.recordedContext`), and the loop records what it
|
|
35
|
+
* learns about files and finished work the same way.
|
|
101
36
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
37
|
+
* `request` is the projected history. `recorded` are entries the loop has just
|
|
38
|
+
* recorded: they join the history at the end of this call, so they go ahead
|
|
39
|
+
* of the first request-only block already in `request` (`tail` of them, at the
|
|
40
|
+
* end), which is where the next request will find them.
|
|
104
41
|
*/
|
|
105
|
-
export
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
end: Message | undefined,
|
|
119
|
-
blocks: readonly Message[],
|
|
120
|
-
replaceable: readonly ReplaceableBlock[] = [],
|
|
121
|
-
retractions: Readonly<Record<string, Message>> = {},
|
|
122
|
-
): void {
|
|
123
|
-
const index = (message: Message | undefined) =>
|
|
124
|
-
message ? history.lastIndexOf(message) : history.length - 1
|
|
125
|
-
const operator = pendingOperatorMessage(history)
|
|
126
|
-
const positionOf = (slot: PlacedBlock) =>
|
|
127
|
-
'before' in slot.at ? index(slot.at.before) : index(slot.at.after) + 1
|
|
128
|
-
const anchorOf = (slot: PlacedBlock) => ('before' in slot.at ? slot.at.before : slot.at.after)
|
|
129
|
-
const next = new Map<string, PlacedBlock>()
|
|
130
|
-
const inserts: { at: number; message: Message }[] = []
|
|
131
|
-
for (const message of blocks) {
|
|
132
|
-
const text = textOf(message)
|
|
133
|
-
if (next.has(text)) continue
|
|
134
|
-
let slot: PlacedBlock | undefined = this.placed.get(text)
|
|
135
|
-
if (slot) {
|
|
136
|
-
const anchor = anchorOf(slot)
|
|
137
|
-
if (anchor && index(anchor) < 0) slot = { text, at: { after: end } }
|
|
138
|
-
} else {
|
|
139
|
-
slot = { text, at: operator ? { before: operator } : { after: end } }
|
|
140
|
-
}
|
|
141
|
-
next.set(text, slot)
|
|
142
|
-
inserts.push({ at: positionOf(slot), message })
|
|
143
|
-
}
|
|
144
|
-
this.placed = next
|
|
145
|
-
|
|
146
|
-
const offered = new Map(replaceable.map((block) => [block.key, block.message]))
|
|
147
|
-
for (const key of new Set([...this.versions.keys(), ...offered.keys()])) {
|
|
148
|
-
const held = this.versions.get(key) ?? []
|
|
149
|
-
const wanted = offered.get(key) ?? retractions[key]
|
|
150
|
-
const last = held.at(-1)
|
|
151
|
-
if (wanted !== undefined && (last === undefined || last.text !== textOf(wanted)))
|
|
152
|
-
held.push({
|
|
153
|
-
text: textOf(wanted),
|
|
154
|
-
message: wanted,
|
|
155
|
-
// The first version goes where the other blocks go; a later one
|
|
156
|
-
// follows the history, after the one it replaces.
|
|
157
|
-
at: last === undefined && operator ? { before: operator } : { after: end },
|
|
158
|
-
})
|
|
159
|
-
// An earlier version whose place a compaction rewrote is given up: the
|
|
160
|
-
// request is rewritten at that point anyway, and stacking stale copies
|
|
161
|
-
// at the end would only add bulk. The latest version is never given up.
|
|
162
|
-
const kept = held.filter((version, i) => {
|
|
163
|
-
if (i === held.length - 1) return true
|
|
164
|
-
const anchor = anchorOf(version)
|
|
165
|
-
return !anchor || index(anchor) >= 0
|
|
166
|
-
})
|
|
167
|
-
// Only the latest and a few earlier versions are carried, so a long turn
|
|
168
|
-
// does not grow by a full copy per change.
|
|
169
|
-
kept.splice(0, Math.max(0, kept.length - (EARLIER_VERSIONS_KEPT + 1)))
|
|
170
|
-
this.versions.set(key, kept)
|
|
171
|
-
for (const version of kept) {
|
|
172
|
-
const anchor = anchorOf(version)
|
|
173
|
-
if (anchor && index(anchor) < 0) version.at = { after: end }
|
|
174
|
-
inserts.push({ at: positionOf(version), message: version.message })
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Back to front, so an earlier position is not shifted by a later one;
|
|
179
|
-
// blocks at one position keep the order they were offered in.
|
|
180
|
-
const order = inserts.map((entry, position) => ({ ...entry, position }))
|
|
181
|
-
order.sort((a, b) => b.at - a.at || b.position - a.position)
|
|
182
|
-
for (const entry of order) request.splice(entry.at, 0, entry.message)
|
|
42
|
+
export function placeRequestContext(
|
|
43
|
+
request: Message[],
|
|
44
|
+
recorded: readonly Message[],
|
|
45
|
+
tail: number,
|
|
46
|
+
blocks: readonly Message[],
|
|
47
|
+
): void {
|
|
48
|
+
if (recorded.length > 0) request.splice(request.length - tail, 0, ...recorded)
|
|
49
|
+
const seen = new Set<string>()
|
|
50
|
+
for (const message of blocks) {
|
|
51
|
+
const text = textOf(message)
|
|
52
|
+
if (seen.has(text)) continue
|
|
53
|
+
seen.add(text)
|
|
54
|
+
request.push(message)
|
|
183
55
|
}
|
|
184
56
|
}
|
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
import { resolveContextWindow } from '../../../compaction/context-window.js'
|
|
2
|
-
import { estimateMessageTokens } from '../../../compaction/token-estimate.js'
|
|
2
|
+
import { estimateMessageTokens, estimateTextTokens } from '../../../compaction/token-estimate.js'
|
|
3
3
|
import { NAMZU } from '../../../constants/telemetry/index.js'
|
|
4
4
|
import { renderSkillsSection } from '../../../persona/assembler.js'
|
|
5
|
+
import { recordedOwnedTaskIds } from '../../../scheduler/completion-inbox.js'
|
|
5
6
|
import { PreparationContextError } from '../../../turn/preparation-context-error.js'
|
|
6
7
|
import {
|
|
7
8
|
type Message,
|
|
8
9
|
type UserMessage,
|
|
9
10
|
createSystemMessage,
|
|
11
|
+
turnContextEntries,
|
|
10
12
|
} from '../../../types/message/index.js'
|
|
11
13
|
import type { ToolChoice } from '../../../types/provider/chat.js'
|
|
12
14
|
import type {
|
|
13
15
|
PrepareStepContext,
|
|
14
16
|
PrepareStepResult,
|
|
17
|
+
RecordedContextItem,
|
|
15
18
|
StepResult,
|
|
16
19
|
StepVeto,
|
|
17
20
|
} from '../../../types/session/index.js'
|
|
18
21
|
import type { Skill } from '../../../types/skills/index.js'
|
|
19
22
|
import { toErrorMessage } from '../../../utils/error.js'
|
|
20
23
|
import { createCallbackInference } from '../callback-inference.js'
|
|
24
|
+
import { recordFileEvidence } from '../file-evidence-context.js'
|
|
21
25
|
import { measureContext } from './phases/compaction.js'
|
|
22
26
|
import type { IterationContext } from './phases/index.js'
|
|
23
|
-
import {
|
|
24
|
-
type ReplaceableBlock,
|
|
25
|
-
placeBeforeLatestOperatorMessage,
|
|
26
|
-
stepContextMessage,
|
|
27
|
-
} from './step-context.js'
|
|
27
|
+
import { stepContextMessage } from './step-context.js'
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* How the next request is shaped: admission, preparation, and the context
|
|
@@ -48,57 +48,68 @@ export interface StepShaping {
|
|
|
48
48
|
|
|
49
49
|
export { stepContextMessage }
|
|
50
50
|
|
|
51
|
-
/**
|
|
52
|
-
export function appendWorkContext(
|
|
53
|
-
shaping: StepShaping,
|
|
54
|
-
messages: Message[],
|
|
55
|
-
stepNumber: number,
|
|
56
|
-
prepared: PrepareStepResult,
|
|
57
|
-
): void {
|
|
58
|
-
// Per-turn data goes in front of the operator's new message while that is
|
|
59
|
-
// what the model is answering, like the other blocks of request-only data.
|
|
60
|
-
placeBeforeLatestOperatorMessage(
|
|
61
|
-
messages,
|
|
62
|
-
workContextBlocks(shaping, messages, stepNumber, prepared).map((block) => block.message),
|
|
63
|
-
)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** What the two work-context blocks report; each tells the model a later one replaces it. */
|
|
51
|
+
/** The names the two kinds of recorded work context are filed under. */
|
|
67
52
|
export const OWNED_WORK_KEY = 'owned-work'
|
|
68
53
|
export const FILE_EVIDENCE_KEY = 'file-evidence'
|
|
69
54
|
|
|
55
|
+
/** What the work context says for one request. */
|
|
56
|
+
export interface WorkContext {
|
|
57
|
+
/** Facts that do not change once true, to be kept in the conversation. */
|
|
58
|
+
readonly recorded: RecordedContextItem[]
|
|
59
|
+
/** Facts about now, shown with this request only. */
|
|
60
|
+
readonly requestOnly: Message[]
|
|
61
|
+
}
|
|
62
|
+
|
|
70
63
|
/**
|
|
71
|
-
* The work
|
|
72
|
-
*
|
|
64
|
+
* The work context for a request whose projected messages are `messages`.
|
|
65
|
+
*
|
|
66
|
+
* Two kinds of fact, each where it belongs. What holds until something
|
|
67
|
+
* changes it, or never changes, is recorded: the file entries that are new or
|
|
68
|
+
* changed since the last record (with the paths whose entry no longer holds),
|
|
69
|
+
* and each owned task once it has finished. What is true only now, the tasks
|
|
70
|
+
* still running, goes with this request alone. Each text is admitted whole or
|
|
71
|
+
* not at all, within the room the request has left.
|
|
73
72
|
*/
|
|
74
|
-
export function
|
|
73
|
+
export function workContext(
|
|
75
74
|
shaping: StepShaping,
|
|
76
75
|
messages: readonly Message[],
|
|
77
76
|
stepNumber: number,
|
|
78
77
|
prepared: PrepareStepResult,
|
|
79
|
-
):
|
|
78
|
+
): WorkContext {
|
|
80
79
|
const { ctx } = shaping
|
|
80
|
+
const held = (key: string) => turnContextEntries(ctx.recorder.messages, key)
|
|
81
|
+
const inbox = ctx.completionInbox
|
|
81
82
|
|
|
82
|
-
const contributions =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
83
|
+
const contributions: { content: string | undefined; key?: string }[] = [
|
|
84
|
+
{ content: inbox?.describeRunningWork() },
|
|
85
|
+
{
|
|
86
|
+
content: inbox?.describeSettledWork(recordedOwnedTaskIds(held(OWNED_WORK_KEY))),
|
|
87
|
+
key: OWNED_WORK_KEY,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
content: recordFileEvidence(
|
|
91
|
+
ctx.toolExecutor.fileEvidenceEntries(messages),
|
|
92
|
+
held(FILE_EVIDENCE_KEY),
|
|
93
|
+
),
|
|
94
|
+
key: FILE_EVIDENCE_KEY,
|
|
95
|
+
},
|
|
96
|
+
]
|
|
97
|
+
const present = contributions.filter((entry) => Boolean(entry.content))
|
|
98
|
+
const work: WorkContext = { recorded: [], requestOnly: [] }
|
|
99
|
+
if (present.length === 0) return work
|
|
90
100
|
let room = stepContext(shaping, stepNumber, prepared).contextBudget?.remainingTokens ?? 0
|
|
91
101
|
// Leave room for the actual task; admit whole contributions, never dangling partial references.
|
|
92
|
-
if (room < 1_500) return
|
|
93
|
-
for (const
|
|
102
|
+
if (room < 1_500) return work
|
|
103
|
+
for (const { content, key } of present) {
|
|
94
104
|
if (!content || content.length > 8_000) continue
|
|
95
105
|
const message = stepContextMessage(content)
|
|
96
106
|
const tokens = estimateMessageTokens(message)
|
|
97
107
|
if (tokens > Math.min(2_000, room - 1_000)) continue
|
|
98
|
-
|
|
108
|
+
if (key) work.recorded.push({ key, text: content })
|
|
109
|
+
else work.requestOnly.push(message)
|
|
99
110
|
room -= tokens
|
|
100
111
|
}
|
|
101
|
-
return
|
|
112
|
+
return work
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
export function stepContext(
|
|
@@ -122,7 +133,8 @@ export function stepContext(
|
|
|
122
133
|
const preamble = [prepared.system, skills].filter(Boolean).join('\n\n')
|
|
123
134
|
const preparedTokens =
|
|
124
135
|
(preamble ? estimateMessageTokens(createSystemMessage(preamble)) : 0) +
|
|
125
|
-
(prepared.context ? estimateMessageTokens(stepContextMessage(prepared.context)) : 0)
|
|
136
|
+
(prepared.context ? estimateMessageTokens(stepContextMessage(prepared.context)) : 0) +
|
|
137
|
+
(prepared.recordedContext ?? []).reduce((sum, item) => sum + estimateTextTokens(item.text), 0)
|
|
126
138
|
const responseReserve = Math.min(
|
|
127
139
|
prepared.maxResponseTokens ?? ctx.turnConfig.maxResponseTokens ?? Math.floor(window.tokens / 4),
|
|
128
140
|
Math.floor(window.tokens / 4),
|
|
@@ -180,6 +192,7 @@ export async function prepareStep(
|
|
|
180
192
|
model?: string
|
|
181
193
|
system?: string
|
|
182
194
|
context?: string
|
|
195
|
+
recordedContext?: readonly RecordedContextItem[]
|
|
183
196
|
skills?: readonly Skill[]
|
|
184
197
|
temperature?: number
|
|
185
198
|
maxResponseTokens?: number
|
|
@@ -206,7 +219,16 @@ export async function prepareStep(
|
|
|
206
219
|
...stepContext(shaping, stepNumber, result),
|
|
207
220
|
generateText: inference.generateText,
|
|
208
221
|
})
|
|
209
|
-
if (decided)
|
|
222
|
+
if (decided) {
|
|
223
|
+
// Recorded context adds up across stages: each stage files its own
|
|
224
|
+
// keys, and one stage must not erase what an earlier one recorded.
|
|
225
|
+
const recorded = [...(result.recordedContext ?? []), ...(decided.recordedContext ?? [])]
|
|
226
|
+
result = {
|
|
227
|
+
...result,
|
|
228
|
+
...decided,
|
|
229
|
+
...(recorded.length > 0 ? { recordedContext: recorded } : {}),
|
|
230
|
+
}
|
|
231
|
+
}
|
|
210
232
|
await selectContextModel(shaping, result.model ?? ctx.turnConfig.model)
|
|
211
233
|
} catch (err) {
|
|
212
234
|
// Skipped, and the rest still run: one broken concern must
|
|
@@ -242,6 +264,7 @@ export async function prepareStep(
|
|
|
242
264
|
model?: string
|
|
243
265
|
system?: string
|
|
244
266
|
context?: string
|
|
267
|
+
recordedContext?: readonly RecordedContextItem[]
|
|
245
268
|
skills?: readonly Skill[]
|
|
246
269
|
temperature?: number
|
|
247
270
|
maxResponseTokens?: number
|
|
@@ -279,6 +302,7 @@ export async function prepareStep(
|
|
|
279
302
|
if (result.model !== undefined) prepared.model = result.model
|
|
280
303
|
if (result.system !== undefined) prepared.system = result.system
|
|
281
304
|
if (result.context !== undefined) prepared.context = result.context
|
|
305
|
+
if (result.recordedContext !== undefined) prepared.recordedContext = result.recordedContext
|
|
282
306
|
if (result.skills !== undefined) prepared.skills = result.skills
|
|
283
307
|
if (result.temperature !== undefined) prepared.temperature = result.temperature
|
|
284
308
|
if (result.maxResponseTokens !== undefined) {
|