dsh-context-mode 0.4.0 → 0.5.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/README.md +10 -61
- package/package.json +1 -16
- package/lib/types/compaction.d.ts +0 -115
- package/lib/types/compaction.d.ts.map +0 -1
- package/lib/types/compaction.js +0 -169
- package/lib/types/transcript.d.ts +0 -96
- package/lib/types/transcript.d.ts.map +0 -1
- package/lib/types/transcript.js +0 -325
package/README.md
CHANGED
|
@@ -132,71 +132,20 @@ only as whatever the summarizing model chose to keep. `precompact` files that
|
|
|
132
132
|
same span into the knowledge base beforehand, but nothing told the model so,
|
|
133
133
|
and a summary that omits a detail reads as if the detail never existed.
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
the shipped `BasicCompactionEngine` and replaces the lossy summary with the
|
|
137
|
-
conversation itself, plus a pointer to everything it did not carry:
|
|
135
|
+
Closing that gap is a **separate product**, not part of this package:
|
|
138
136
|
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
Each checkpoint it writes carries three parts:
|
|
144
|
-
|
|
145
|
-
| Part | Content |
|
|
146
|
-
| --- | --- |
|
|
147
|
-
| The shipped summary | The eight-section checkpoint, unchanged |
|
|
148
|
-
| `## Conversation Transcript` | user turns kept whole; assistant replies kept head+tail; tool output reduced to one index line each |
|
|
149
|
-
| `## Archive Index` | The `source` labels to search, and how to query them |
|
|
150
|
-
|
|
151
|
-
Every region that was clipped or dropped leaves a pointer naming the `source`
|
|
152
|
-
holding the original and the `seq` it came from, so the checkpoint says *which*
|
|
153
|
-
event lost detail rather than merely that an archive exists:
|
|
154
|
-
|
|
155
|
-
```
|
|
156
|
-
## [assistant 447]
|
|
157
|
-
… first 200 chars …
|
|
158
|
-
[... 1581 of 1981 chars elided from seq 447; retrieve with ctx_search(source: "session/<id>/narrative") ...]
|
|
159
|
-
… last 200 chars …
|
|
160
|
-
|
|
161
|
-
## [tool ctx_execute seq 3424, 29808 chars → search `session/<id>/finding`]
|
|
137
|
+
```sh
|
|
138
|
+
npm install dsh-context-mode-compaction
|
|
139
|
+
npx dsh-context-mode-compaction
|
|
162
140
|
```
|
|
163
141
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
one requires:
|
|
169
|
-
|
|
170
|
-
```yaml
|
|
171
|
-
- id: compaction
|
|
172
|
-
name: cordis:group
|
|
173
|
-
group: true
|
|
174
|
-
isolate:
|
|
175
|
-
compaction: true
|
|
176
|
-
toolResultPruner: true
|
|
177
|
-
config:
|
|
178
|
-
- id: compaction-basic
|
|
179
|
-
name: 'dsh-context-mode/compaction' # was @deepseek-ai/dsh-compaction-basic
|
|
180
|
-
config:
|
|
181
|
-
thresholdRatio: 0.8 # compact at 80% of the window
|
|
182
|
-
retainRatio: 0.1 # keep the newest 10% verbatim
|
|
183
|
-
|
|
184
|
-
- id: command-compact
|
|
185
|
-
name: '@deepseek-ai/dsh-command-compact'
|
|
186
|
-
```
|
|
142
|
+
`dsh-context-mode-compaction` supplies the compaction engine that writes the
|
|
143
|
+
transcript and the index; this package supplies the archiver that stores what
|
|
144
|
+
those pointers name, and the `ctx_search` tool that reads it back. The two are
|
|
145
|
+
independent — install both for the full path.
|
|
187
146
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
prefix-cache-aligned replay all stay on the shipped engine. The appendices are
|
|
191
|
-
added to the returned summary rather than injected into the summarization
|
|
192
|
-
instruction, so the shipped output contract is untouched and no model has to
|
|
193
|
-
follow an amended format. Failing to build them returns the superseded summary
|
|
194
|
-
unchanged, and restoring the shipped `name` row disables them entirely.
|
|
195
|
-
|
|
196
|
-
`TRANSCRIPT` and per-entry clipping are tunable from
|
|
197
|
-
`dsh-context-mode/transcript`: `USER_CLIP_AT` / `USER_CLIP_KEEP` for long user
|
|
198
|
-
turns, `ASSISTANT_KEEP` for assistant head+tail, and `MAX_TRANSCRIPT_CHARS` for
|
|
199
|
-
the whole body.
|
|
147
|
+
See [dsh-context-mode-compaction](https://www.npmjs.com/package/dsh-context-mode-compaction)
|
|
148
|
+
for the checkpoint format, the tuning constants, and the preset wiring.
|
|
200
149
|
|
|
201
150
|
## Development
|
|
202
151
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-context-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Expose context-mode MCP tools as native DeepSeek Harness tools",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -24,14 +24,6 @@
|
|
|
24
24
|
"types": "./lib/types/index.d.ts",
|
|
25
25
|
"import": "./lib/types/index.js"
|
|
26
26
|
},
|
|
27
|
-
"./compaction": {
|
|
28
|
-
"types": "./lib/types/compaction.d.ts",
|
|
29
|
-
"import": "./lib/types/compaction.js"
|
|
30
|
-
},
|
|
31
|
-
"./transcript": {
|
|
32
|
-
"types": "./lib/types/transcript.d.ts",
|
|
33
|
-
"import": "./lib/types/transcript.js"
|
|
34
|
-
},
|
|
35
27
|
"./package.json": "./package.json",
|
|
36
28
|
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
37
29
|
},
|
|
@@ -67,20 +59,13 @@
|
|
|
67
59
|
},
|
|
68
60
|
"peerDependencies": {
|
|
69
61
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
70
|
-
"@deepseek-ai/dsh-compaction-basic": "^0.1.5-rc.2",
|
|
71
62
|
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
|
|
72
63
|
"@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.2",
|
|
73
64
|
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2"
|
|
74
65
|
},
|
|
75
|
-
"peerDependenciesMeta": {
|
|
76
|
-
"@deepseek-ai/dsh-compaction-basic": {
|
|
77
|
-
"optional": true
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
66
|
"devDependencies": {
|
|
81
67
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
82
68
|
"@deepseek-ai/dsh-agent": "0.1.5-rc.2",
|
|
83
|
-
"@deepseek-ai/dsh-compaction-basic": "0.1.5-rc.2",
|
|
84
69
|
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
|
|
85
70
|
"@deepseek-ai/dsh-skill": "^0.1.5-rc.2",
|
|
86
71
|
"@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.2",
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compaction backend that appends an archive index to every checkpoint.
|
|
3
|
-
*
|
|
4
|
-
* DSH's shipped backend condenses an older span of the conversation into a
|
|
5
|
-
* summary and lets the raw events fall out of the derived history. The summary
|
|
6
|
-
* is lossy by design: its instruction fixes the sections a checkpoint may
|
|
7
|
-
* carry, and tool output has no section of its own, so a long tool result
|
|
8
|
-
* survives only as whatever the summarizing model chose to keep.
|
|
9
|
-
*
|
|
10
|
-
* This plugin's `precompact` listener files the same span into the
|
|
11
|
-
* context-mode knowledge base first, so the detail still exists and
|
|
12
|
-
* `ctx_search` can reach it. What it cannot do from outside is tell the model
|
|
13
|
-
* that — the checkpoint text is written here, inside the engine, and the
|
|
14
|
-
* shipped engine has no reason to mention a knowledge base it does not know
|
|
15
|
-
* about. That is the gap this subclass closes.
|
|
16
|
-
*
|
|
17
|
-
* Two deliberate choices keep the risk low:
|
|
18
|
-
*
|
|
19
|
-
* - Only `summarize()` is overridden. Trigger policy, retention, the
|
|
20
|
-
* bracket-first transaction, token metering, and KV-cache-aligned replay
|
|
21
|
-
* all stay on the shipped implementation, so this cannot diverge from the
|
|
22
|
-
* behavior the rest of DSH expects.
|
|
23
|
-
* - The index is appended to the *returned* summary, never injected into the
|
|
24
|
-
* summarization instruction. The summarizing model never sees this text,
|
|
25
|
-
* so the shipped output contract ("keep every section, in order") stays
|
|
26
|
-
* intact and no model has to be trusted to follow an amended format.
|
|
27
|
-
*
|
|
28
|
-
* Nothing here is allowed to fail a compaction: if the index cannot be built,
|
|
29
|
-
* the superseded summary is returned unchanged.
|
|
30
|
-
*
|
|
31
|
-
* @module dsh-context-mode/compaction
|
|
32
|
-
*/
|
|
33
|
-
import type { ContentBlock } from '@deepseek-ai/dsh-llm';
|
|
34
|
-
import { BasicCompactionEngine } from '@deepseek-ai/dsh-compaction-basic';
|
|
35
|
-
/**
|
|
36
|
-
* The shipped summarization input and result types, derived from the base
|
|
37
|
-
* class rather than restated.
|
|
38
|
-
*
|
|
39
|
-
* The package does not re-export them from its root, and deep-importing its
|
|
40
|
-
* private `lib/types/summarizer.js` path would break on any internal move.
|
|
41
|
-
* Deriving from the method signature keeps this module aligned with whatever
|
|
42
|
-
* the installed version declares, and needs no import of its own.
|
|
43
|
-
*/
|
|
44
|
-
type SummarizeArgs = Parameters<BasicCompactionEngine['summarize']>;
|
|
45
|
-
type SummarizedResult = Awaited<ReturnType<BasicCompactionEngine['summarize']>>;
|
|
46
|
-
/** Session-event shapes this module reads. */
|
|
47
|
-
interface SessionEventLike {
|
|
48
|
-
readonly type: string;
|
|
49
|
-
readonly seq?: number;
|
|
50
|
-
readonly data?: unknown;
|
|
51
|
-
}
|
|
52
|
-
interface SessionLike {
|
|
53
|
-
readonly id?: string;
|
|
54
|
-
snapshotEvents(): readonly SessionEventLike[];
|
|
55
|
-
/** Current surface node sequence; the compacted span is a slice of it. */
|
|
56
|
-
readonly surface?: {
|
|
57
|
-
readonly nodes: readonly number[];
|
|
58
|
-
};
|
|
59
|
-
/** One event by sequence number, or undefined when absent. */
|
|
60
|
-
eventAt?(seq: number): SessionEventLike | undefined;
|
|
61
|
-
}
|
|
62
|
-
interface AgentLike {
|
|
63
|
-
readonly session?: SessionLike;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* A compaction engine that names the session archive in each checkpoint.
|
|
67
|
-
*
|
|
68
|
-
* Constructed by DSH exactly like the shipped engine it extends, so the row
|
|
69
|
-
* that mounts it needs no additional wiring.
|
|
70
|
-
*/
|
|
71
|
-
export declare class DshContextModeCompaction extends BasicCompactionEngine {
|
|
72
|
-
#private;
|
|
73
|
-
/** Capture the compacted range for the summarizer, then run the shipped path. */
|
|
74
|
-
compactRegion(...args: Parameters<BasicCompactionEngine['compactRegion']>): ReturnType<BasicCompactionEngine['compactRegion']>;
|
|
75
|
-
/**
|
|
76
|
-
* Summarize the replayed region, then append the transcript and archive index.
|
|
77
|
-
*
|
|
78
|
-
* The shipped call runs first and unmodified, so prefix-cache alignment,
|
|
79
|
-
* token accounting, and the returned `SummaryResult` envelope are unchanged.
|
|
80
|
-
*/
|
|
81
|
-
protected summarize(input: SummarizeArgs[0], agent: SummarizeArgs[1], signal?: SummarizeArgs[2]): Promise<SummarizedResult>;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Build the archive-index block for one agent's session.
|
|
85
|
-
*
|
|
86
|
-
* The source labels are derivable without waiting on the archiver: the
|
|
87
|
-
* `precompact` listener files each layer under `session/<id>/<layer>`, and the
|
|
88
|
-
* session id is available here. The index therefore states *where* the detail
|
|
89
|
-
* lives rather than claiming anything about what it contains.
|
|
90
|
-
*
|
|
91
|
-
* @param agent - owner of the session being compacted.
|
|
92
|
-
* @returns the markdown block, or an empty string when no session is reachable.
|
|
93
|
-
*/
|
|
94
|
-
export declare function buildArchiveIndex(agent: AgentLike): string;
|
|
95
|
-
/**
|
|
96
|
-
* Build the archive-index block for one archive source root.
|
|
97
|
-
*
|
|
98
|
-
* @param base - archive source root, e.g. `session/<id>`.
|
|
99
|
-
* @returns the markdown block.
|
|
100
|
-
*/
|
|
101
|
-
export declare function buildArchiveIndexFromBase(base: string): string;
|
|
102
|
-
/**
|
|
103
|
-
* Append an index block to the text of a summary.
|
|
104
|
-
*
|
|
105
|
-
* Only text blocks are touched. A summary may also carry non-text blocks, and
|
|
106
|
-
* rewriting or dropping those is the shipped engine's business, not this
|
|
107
|
-
* module's; they are copied through untouched.
|
|
108
|
-
*
|
|
109
|
-
* @param summary - the superseded summary blocks.
|
|
110
|
-
* @param index - the block to append.
|
|
111
|
-
* @returns new blocks with the index appended to the trailing text.
|
|
112
|
-
*/
|
|
113
|
-
export declare function appendToSummary(summary: readonly ContentBlock[], index: string): ContentBlock[];
|
|
114
|
-
export default DshContextModeCompaction;
|
|
115
|
-
//# sourceMappingURL=compaction.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../src/compaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAA;AAQzE;;;;;;;;GAQG;AACH,KAAK,aAAa,GAAG,UAAU,CAAC,qBAAqB,CAAC,WAAW,CAAC,CAAC,CAAA;AACnE,KAAK,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,qBAAqB,CAAC,WAAW,CAAC,CAAC,CAAC,CAAA;AAQ/E,8CAA8C;AAC9C,UAAU,gBAAgB;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,UAAU,WAAW;IACnB,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,IAAI,SAAS,gBAAgB,EAAE,CAAA;IAC7C,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,CAAA;IACxD,8DAA8D;IAC9D,OAAO,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAAA;CACpD;AAED,UAAU,SAAS;IACjB,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,CAAA;CAC/B;AAQD;;;;;GAKG;AACH,qBAAa,wBAAyB,SAAQ,qBAAqB;;IAWjE,iFAAiF;IAClE,aAAa,CAC1B,GAAG,IAAI,EAAE,UAAU,CAAC,qBAAqB,CAAC,eAAe,CAAC,CAAC,GAC1D,UAAU,CAAC,qBAAqB,CAAC,eAAe,CAAC,CAAC;IAUrD;;;;;OAKG;cACsB,SAAS,CAChC,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,EACvB,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,EACvB,MAAM,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,gBAAgB,CAAC;CAe7B;AAqDD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAG1D;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAiB9D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,SAAS,YAAY,EAAE,EAChC,KAAK,EAAE,MAAM,GACZ,YAAY,EAAE,CAUhB;AAED,eAAe,wBAAwB,CAAA"}
|
package/lib/types/compaction.js
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { BasicCompactionEngine } from '@deepseek-ai/dsh-compaction-basic';
|
|
2
|
-
import { MAX_TRANSCRIPT_CHARS, buildTranscript, renderTranscript, } from './transcript.js';
|
|
3
|
-
/** Heading of the appended section. Kept short; it costs context on every later request. */
|
|
4
|
-
const INDEX_HEADING = '## Archive Index';
|
|
5
|
-
/** Upper bound on the appended section, so a checkpoint can never be grown without limit. */
|
|
6
|
-
const MAX_INDEX_CHARS = 1_200;
|
|
7
|
-
/**
|
|
8
|
-
* A compaction engine that names the session archive in each checkpoint.
|
|
9
|
-
*
|
|
10
|
-
* Constructed by DSH exactly like the shipped engine it extends, so the row
|
|
11
|
-
* that mounts it needs no additional wiring.
|
|
12
|
-
*/
|
|
13
|
-
export class DshContextModeCompaction extends BasicCompactionEngine {
|
|
14
|
-
/**
|
|
15
|
-
* Surface range of the compaction in flight.
|
|
16
|
-
*
|
|
17
|
-
* `summarize()` receives only the replayed messages, not the sequence
|
|
18
|
-
* numbers behind them, so the range is captured here — at the one seam that
|
|
19
|
-
* knows it — and read back inside `summarize()`. Only two numbers are
|
|
20
|
-
* stashed; the shipped transaction, selection, and validation are untouched.
|
|
21
|
-
*/
|
|
22
|
-
#range;
|
|
23
|
-
/** Capture the compacted range for the summarizer, then run the shipped path. */
|
|
24
|
-
async compactRegion(...args) {
|
|
25
|
-
this.#range = { start: args[0], end: args[1] };
|
|
26
|
-
try {
|
|
27
|
-
return await super.compactRegion(...args);
|
|
28
|
-
}
|
|
29
|
-
finally {
|
|
30
|
-
// Cleared unconditionally: a stale range must never label a later summary.
|
|
31
|
-
this.#range = undefined;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Summarize the replayed region, then append the transcript and archive index.
|
|
36
|
-
*
|
|
37
|
-
* The shipped call runs first and unmodified, so prefix-cache alignment,
|
|
38
|
-
* token accounting, and the returned `SummaryResult` envelope are unchanged.
|
|
39
|
-
*/
|
|
40
|
-
async summarize(input, agent, signal) {
|
|
41
|
-
const result = await super.summarize(input, agent, signal);
|
|
42
|
-
try {
|
|
43
|
-
const session = agent.session;
|
|
44
|
-
const base = archiveBase(session);
|
|
45
|
-
if (base === undefined)
|
|
46
|
-
return result;
|
|
47
|
-
const appended = buildAppendices(session, this.#range, base);
|
|
48
|
-
if (appended.length === 0)
|
|
49
|
-
return result;
|
|
50
|
-
return { ...result, summary: appendToSummary(result.summary, appended.join('\n\n')) };
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
// An appendix is an improvement, never a requirement: a failure here must
|
|
54
|
-
// not turn into a failed compaction.
|
|
55
|
-
return result;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
/** Archive source root for one session, or undefined when it cannot be named. */
|
|
60
|
-
function archiveBase(session) {
|
|
61
|
-
const id = session?.id;
|
|
62
|
-
if (typeof id !== 'string' || id.length === 0)
|
|
63
|
-
return undefined;
|
|
64
|
-
return `session/${id}`;
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Build the transcript and index blocks appended below the summary.
|
|
68
|
-
*
|
|
69
|
-
* The compacted events are resolved from the captured range against the
|
|
70
|
-
* session's own surface, so a range that no longer matches yields no
|
|
71
|
-
* transcript rather than a mislabelled one.
|
|
72
|
-
*/
|
|
73
|
-
function buildAppendices(session, range, base) {
|
|
74
|
-
const blocks = [];
|
|
75
|
-
const events = compactedEvents(session, range);
|
|
76
|
-
if (events.length > 0) {
|
|
77
|
-
const lines = buildTranscript(events, { base });
|
|
78
|
-
const body = renderTranscript(lines, MAX_TRANSCRIPT_CHARS);
|
|
79
|
-
if (body.length > 0)
|
|
80
|
-
blocks.push(`## Conversation Transcript\n\n${body}`);
|
|
81
|
-
}
|
|
82
|
-
const index = buildArchiveIndexFromBase(base);
|
|
83
|
-
if (index.length > 0)
|
|
84
|
-
blocks.push(index);
|
|
85
|
-
return blocks;
|
|
86
|
-
}
|
|
87
|
-
/** Resolve the compacted events for a captured surface range. */
|
|
88
|
-
function compactedEvents(session, range) {
|
|
89
|
-
if (session === undefined)
|
|
90
|
-
return [];
|
|
91
|
-
const eventAt = session.eventAt;
|
|
92
|
-
const nodes = session.surface?.nodes;
|
|
93
|
-
// Without a range or a live surface, fall back to nothing rather than
|
|
94
|
-
// guessing: a transcript of the wrong span is worse than no transcript.
|
|
95
|
-
if (range === undefined || eventAt === undefined || nodes === undefined)
|
|
96
|
-
return [];
|
|
97
|
-
const out = [];
|
|
98
|
-
for (const seq of nodes) {
|
|
99
|
-
if (seq < range.start || seq > range.end)
|
|
100
|
-
continue;
|
|
101
|
-
const event = eventAt.call(session, seq);
|
|
102
|
-
if (event !== undefined)
|
|
103
|
-
out.push(event);
|
|
104
|
-
}
|
|
105
|
-
return out;
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Build the archive-index block for one agent's session.
|
|
109
|
-
*
|
|
110
|
-
* The source labels are derivable without waiting on the archiver: the
|
|
111
|
-
* `precompact` listener files each layer under `session/<id>/<layer>`, and the
|
|
112
|
-
* session id is available here. The index therefore states *where* the detail
|
|
113
|
-
* lives rather than claiming anything about what it contains.
|
|
114
|
-
*
|
|
115
|
-
* @param agent - owner of the session being compacted.
|
|
116
|
-
* @returns the markdown block, or an empty string when no session is reachable.
|
|
117
|
-
*/
|
|
118
|
-
export function buildArchiveIndex(agent) {
|
|
119
|
-
const base = archiveBase(agent.session);
|
|
120
|
-
return base === undefined ? '' : buildArchiveIndexFromBase(base);
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Build the archive-index block for one archive source root.
|
|
124
|
-
*
|
|
125
|
-
* @param base - archive source root, e.g. `session/<id>`.
|
|
126
|
-
* @returns the markdown block.
|
|
127
|
-
*/
|
|
128
|
-
export function buildArchiveIndexFromBase(base) {
|
|
129
|
-
const lines = [
|
|
130
|
-
INDEX_HEADING,
|
|
131
|
-
'',
|
|
132
|
-
'The raw transcript of this span was archived to the context-mode knowledge',
|
|
133
|
-
'base before it was condensed, so detail the summary above omits is still',
|
|
134
|
-
'retrievable with `ctx_search`. Scope each query to one layer by `source`:',
|
|
135
|
-
'',
|
|
136
|
-
`- \`source: "${base}/constraint"\` — user messages: requirements, decisions, limits`,
|
|
137
|
-
`- \`source: "${base}/finding"\` — tool results and stated conclusions`,
|
|
138
|
-
`- \`source: "${base}/narrative"\` — assistant reasoning and plans`,
|
|
139
|
-
'',
|
|
140
|
-
'Search for a concrete token you expect in the original (a command, an error',
|
|
141
|
-
'string, a path, an identifier) rather than a paraphrase of the question.',
|
|
142
|
-
];
|
|
143
|
-
const block = lines.join('\n');
|
|
144
|
-
return block.length <= MAX_INDEX_CHARS ? block : `${block.slice(0, MAX_INDEX_CHARS - 1)}…`;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Append an index block to the text of a summary.
|
|
148
|
-
*
|
|
149
|
-
* Only text blocks are touched. A summary may also carry non-text blocks, and
|
|
150
|
-
* rewriting or dropping those is the shipped engine's business, not this
|
|
151
|
-
* module's; they are copied through untouched.
|
|
152
|
-
*
|
|
153
|
-
* @param summary - the superseded summary blocks.
|
|
154
|
-
* @param index - the block to append.
|
|
155
|
-
* @returns new blocks with the index appended to the trailing text.
|
|
156
|
-
*/
|
|
157
|
-
export function appendToSummary(summary, index) {
|
|
158
|
-
const blocks = summary.map(block => ({ ...block }));
|
|
159
|
-
for (let position = blocks.length - 1; position >= 0; position -= 1) {
|
|
160
|
-
const block = blocks[position];
|
|
161
|
-
if (block.type !== 'text' || typeof block.text !== 'string')
|
|
162
|
-
continue;
|
|
163
|
-
blocks[position] = { ...block, text: `${block.text}\n\n${index}` };
|
|
164
|
-
return blocks;
|
|
165
|
-
}
|
|
166
|
-
// A summary with no text block at all: add one rather than dropping the index.
|
|
167
|
-
return [...blocks, { type: 'text', text: index }];
|
|
168
|
-
}
|
|
169
|
-
export default DshContextModeCompaction;
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Conversation transcript construction for compaction checkpoints.
|
|
3
|
-
*
|
|
4
|
-
* DSH's shipped checkpoint is a lossy summary: its instruction fixes eight
|
|
5
|
-
* sections, tool output has no section of its own, and the summarizer keeps
|
|
6
|
-
* whatever prose it chose. In practice a compacted span loses most of what was
|
|
7
|
-
* actually said.
|
|
8
|
-
*
|
|
9
|
-
* This module rebuilds the *conversation* from the compacted events instead of
|
|
10
|
-
* asking a model to retell it:
|
|
11
|
-
*
|
|
12
|
-
* user/message kept whole — a requirement is a requirement. A very long
|
|
13
|
-
* one is clipped head+tail with an archive pointer.
|
|
14
|
-
* assistant/message head and tail only; the middle is archived and reachable.
|
|
15
|
-
* tool/result never copied. One index line names the tool, its size,
|
|
16
|
-
* and the archive source that holds the output.
|
|
17
|
-
*
|
|
18
|
-
* Every clipped or dropped region leaves a pointer naming the `source` to
|
|
19
|
-
* search and the `seq` it came from, so the checkpoint says *which* event lost
|
|
20
|
-
* detail rather than merely that an archive exists.
|
|
21
|
-
*
|
|
22
|
-
* A prior checkpoint is never transcribed: its own text is already in the
|
|
23
|
-
* summary, and copying it forward would make the transcript grow without bound
|
|
24
|
-
* on every compaction.
|
|
25
|
-
*
|
|
26
|
-
* @module dsh-context-mode/transcript
|
|
27
|
-
*/
|
|
28
|
-
/** Event shapes this module reads. Mirrors the session log. */
|
|
29
|
-
export interface TranscriptEventLike {
|
|
30
|
-
readonly type: string;
|
|
31
|
-
readonly seq?: number;
|
|
32
|
-
readonly data?: unknown;
|
|
33
|
-
}
|
|
34
|
-
/** Head and tail kept for a user message above the clip threshold, in characters. */
|
|
35
|
-
export declare const USER_CLIP_AT = 1000;
|
|
36
|
-
export declare const USER_CLIP_KEEP = 500;
|
|
37
|
-
/** Head and tail kept for every assistant message, in characters. */
|
|
38
|
-
export declare const ASSISTANT_KEEP = 200;
|
|
39
|
-
/** Upper bound on the whole transcript, so one checkpoint cannot grow without limit. */
|
|
40
|
-
export declare const MAX_TRANSCRIPT_CHARS = 400000;
|
|
41
|
-
/** One rendered transcript entry. */
|
|
42
|
-
export interface TranscriptLine {
|
|
43
|
-
readonly kind: 'user' | 'assistant' | 'tool';
|
|
44
|
-
readonly seq: number;
|
|
45
|
-
readonly text: string;
|
|
46
|
-
/** Characters in the source event before any clipping. */
|
|
47
|
-
readonly sourceChars: number;
|
|
48
|
-
}
|
|
49
|
-
/** Options controlling how much of each event survives into the transcript. */
|
|
50
|
-
export interface TranscriptOptions {
|
|
51
|
-
/** Archive source root, e.g. `session/<id>`. */
|
|
52
|
-
readonly base: string;
|
|
53
|
-
/** Include assistant head/tail entries. Defaults to true. */
|
|
54
|
-
readonly keepAssistant?: boolean;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Whether an event is a checkpoint written by a previous compaction.
|
|
58
|
-
*
|
|
59
|
-
* Matched on `source` when the event still carries it, and on the checkpoint
|
|
60
|
-
* preamble otherwise: a compacted replacement message is replayed back through
|
|
61
|
-
* this module as an ordinary `user/message`, so the structural marker is not
|
|
62
|
-
* always present by the time the text is read.
|
|
63
|
-
*/
|
|
64
|
-
export declare function isCheckpointEvent(event: TranscriptEventLike): boolean;
|
|
65
|
-
/**
|
|
66
|
-
* Build the transcript for a compacted span.
|
|
67
|
-
*
|
|
68
|
-
* @param events - the compacted events, in session order.
|
|
69
|
-
* @param options - archive root and assistant policy.
|
|
70
|
-
* @returns the rendered lines, oldest first.
|
|
71
|
-
*/
|
|
72
|
-
export declare function buildTranscript(events: readonly TranscriptEventLike[], options: TranscriptOptions): TranscriptLine[];
|
|
73
|
-
/**
|
|
74
|
-
* The archive pointer left where content was clipped.
|
|
75
|
-
*
|
|
76
|
-
* @param total - characters in the source event.
|
|
77
|
-
* @param dropped - characters not carried into the transcript.
|
|
78
|
-
* @param base - archive source root.
|
|
79
|
-
* @param layer - archive layer holding the original.
|
|
80
|
-
* @param seq - the source event's sequence number.
|
|
81
|
-
* @returns the model-facing notice.
|
|
82
|
-
*/
|
|
83
|
-
export declare function clipNotice(total: string, dropped: string, base: string, layer: string, seq: number): string;
|
|
84
|
-
/**
|
|
85
|
-
* Render transcript lines into one text body, stopping at the size bound.
|
|
86
|
-
*
|
|
87
|
-
* The bound is enforced from the end: the newest entries matter most to a
|
|
88
|
-
* resuming model, so an over-long transcript keeps its tail and reports how
|
|
89
|
-
* many older entries were dropped.
|
|
90
|
-
*
|
|
91
|
-
* @param lines - transcript lines in session order.
|
|
92
|
-
* @param maxChars - upper bound on the rendered body.
|
|
93
|
-
* @returns the rendered body.
|
|
94
|
-
*/
|
|
95
|
-
export declare function renderTranscript(lines: readonly TranscriptLine[], maxChars: number): string;
|
|
96
|
-
//# sourceMappingURL=transcript.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../../src/transcript.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,+DAA+D;AAC/D,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CACxB;AAMD,qFAAqF;AACrF,eAAO,MAAM,YAAY,OAAQ,CAAA;AACjC,eAAO,MAAM,cAAc,MAAM,CAAA;AAEjC,qEAAqE;AACrE,eAAO,MAAM,cAAc,MAAM,CAAA;AAKjC,wFAAwF;AACxF,eAAO,MAAM,oBAAoB,SAAU,CAAA;AAK3C,qCAAqC;AACrC,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,MAAM,CAAA;IAC5C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,0DAA0D;IAC1D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AAED,+EAA+E;AAC/E,MAAM,WAAW,iBAAiB;IAChC,gDAAgD;IAChD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,6DAA6D;IAC7D,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CACjC;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAIrE;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,SAAS,mBAAmB,EAAE,EACtC,OAAO,EAAE,iBAAiB,GACzB,cAAc,EAAE,CAQlB;AAqID;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,GACV,MAAM,CAKR;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAa3F"}
|
package/lib/types/transcript.js
DELETED
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Conversation transcript construction for compaction checkpoints.
|
|
3
|
-
*
|
|
4
|
-
* DSH's shipped checkpoint is a lossy summary: its instruction fixes eight
|
|
5
|
-
* sections, tool output has no section of its own, and the summarizer keeps
|
|
6
|
-
* whatever prose it chose. In practice a compacted span loses most of what was
|
|
7
|
-
* actually said.
|
|
8
|
-
*
|
|
9
|
-
* This module rebuilds the *conversation* from the compacted events instead of
|
|
10
|
-
* asking a model to retell it:
|
|
11
|
-
*
|
|
12
|
-
* user/message kept whole — a requirement is a requirement. A very long
|
|
13
|
-
* one is clipped head+tail with an archive pointer.
|
|
14
|
-
* assistant/message head and tail only; the middle is archived and reachable.
|
|
15
|
-
* tool/result never copied. One index line names the tool, its size,
|
|
16
|
-
* and the archive source that holds the output.
|
|
17
|
-
*
|
|
18
|
-
* Every clipped or dropped region leaves a pointer naming the `source` to
|
|
19
|
-
* search and the `seq` it came from, so the checkpoint says *which* event lost
|
|
20
|
-
* detail rather than merely that an archive exists.
|
|
21
|
-
*
|
|
22
|
-
* A prior checkpoint is never transcribed: its own text is already in the
|
|
23
|
-
* summary, and copying it forward would make the transcript grow without bound
|
|
24
|
-
* on every compaction.
|
|
25
|
-
*
|
|
26
|
-
* @module dsh-context-mode/transcript
|
|
27
|
-
*/
|
|
28
|
-
/** Layer names used in the archive source label. Mirrors `precompact`'s LAYERS. */
|
|
29
|
-
const LAYER_CONSTRAINT = 'constraint';
|
|
30
|
-
const LAYER_FINDING = 'finding';
|
|
31
|
-
/** Head and tail kept for a user message above the clip threshold, in characters. */
|
|
32
|
-
export const USER_CLIP_AT = 1_000;
|
|
33
|
-
export const USER_CLIP_KEEP = 500;
|
|
34
|
-
/** Head and tail kept for every assistant message, in characters. */
|
|
35
|
-
export const ASSISTANT_KEEP = 200;
|
|
36
|
-
/** Assistant messages shorter than this are kept whole rather than split. */
|
|
37
|
-
const ASSISTANT_MIN_SPLIT = ASSISTANT_KEEP * 2;
|
|
38
|
-
/** Upper bound on the whole transcript, so one checkpoint cannot grow without limit. */
|
|
39
|
-
export const MAX_TRANSCRIPT_CHARS = 400_000;
|
|
40
|
-
/** The tag the shipped engine uses; its presence marks a prior checkpoint. */
|
|
41
|
-
const CHECKPOINT_MARKER = 'This is an automatically generated checkpoint';
|
|
42
|
-
/**
|
|
43
|
-
* Whether an event is a checkpoint written by a previous compaction.
|
|
44
|
-
*
|
|
45
|
-
* Matched on `source` when the event still carries it, and on the checkpoint
|
|
46
|
-
* preamble otherwise: a compacted replacement message is replayed back through
|
|
47
|
-
* this module as an ordinary `user/message`, so the structural marker is not
|
|
48
|
-
* always present by the time the text is read.
|
|
49
|
-
*/
|
|
50
|
-
export function isCheckpointEvent(event) {
|
|
51
|
-
const source = sourceOf(event);
|
|
52
|
-
if (source !== undefined && source.kind === 'plugin' && source.plugin === 'compact')
|
|
53
|
-
return true;
|
|
54
|
-
return messageText(event).includes(CHECKPOINT_MARKER);
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Build the transcript for a compacted span.
|
|
58
|
-
*
|
|
59
|
-
* @param events - the compacted events, in session order.
|
|
60
|
-
* @param options - archive root and assistant policy.
|
|
61
|
-
* @returns the rendered lines, oldest first.
|
|
62
|
-
*/
|
|
63
|
-
export function buildTranscript(events, options) {
|
|
64
|
-
const names = toolNames(events);
|
|
65
|
-
const lines = [];
|
|
66
|
-
for (const event of events) {
|
|
67
|
-
const line = transcriptLine(event, options, names);
|
|
68
|
-
if (line !== undefined)
|
|
69
|
-
lines.push(line);
|
|
70
|
-
}
|
|
71
|
-
return lines;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Map each tool-call id to its tool name.
|
|
75
|
-
*
|
|
76
|
-
* A tool call is not a surface event of its own: it is a `tool-call` content
|
|
77
|
-
* block inside an `assistant/message`, which is why the compacted span holds
|
|
78
|
-
* no `tool/call` events at all. The result answers it by id, so the pairing is
|
|
79
|
-
* collected from assistant blocks up front and looked up while rendering.
|
|
80
|
-
*/
|
|
81
|
-
function toolNames(events) {
|
|
82
|
-
const names = new Map();
|
|
83
|
-
for (const event of events) {
|
|
84
|
-
if (event.type !== 'assistant/message')
|
|
85
|
-
continue;
|
|
86
|
-
const data = asRecord(event.data);
|
|
87
|
-
const message = asRecord(data?.message) ?? data;
|
|
88
|
-
const content = message?.content;
|
|
89
|
-
if (!Array.isArray(content))
|
|
90
|
-
continue;
|
|
91
|
-
for (const block of content) {
|
|
92
|
-
if (block === null || typeof block !== 'object')
|
|
93
|
-
continue;
|
|
94
|
-
const record = block;
|
|
95
|
-
if (record.type !== 'tool-call')
|
|
96
|
-
continue;
|
|
97
|
-
// An assistant tool-call block carries `id`; its paired result carries
|
|
98
|
-
// the same value as `toolCallId`, so both spellings are accepted.
|
|
99
|
-
const id = typeof record.id === 'string'
|
|
100
|
-
? record.id
|
|
101
|
-
: typeof record.toolCallId === 'string' ? record.toolCallId : undefined;
|
|
102
|
-
if (id === undefined)
|
|
103
|
-
continue;
|
|
104
|
-
const name = typeof record.name === 'string' ? record.name : undefined;
|
|
105
|
-
if (name !== undefined && name.length > 0)
|
|
106
|
-
names.set(id, name);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return names;
|
|
110
|
-
}
|
|
111
|
-
/** Render one event into a transcript line, or undefined when it contributes nothing. */
|
|
112
|
-
function transcriptLine(event, options, names) {
|
|
113
|
-
const seq = typeof event.seq === 'number' ? event.seq : -1;
|
|
114
|
-
if (event.type === 'tool/result')
|
|
115
|
-
return toolLine(event, seq, options, names);
|
|
116
|
-
// A prior checkpoint carries the previous summary; transcribing it would
|
|
117
|
-
// duplicate the current summary and grow without bound across compactions.
|
|
118
|
-
if (event.type === 'user/message') {
|
|
119
|
-
if (isCheckpointEvent(event))
|
|
120
|
-
return undefined;
|
|
121
|
-
if (isInjected(event))
|
|
122
|
-
return undefined;
|
|
123
|
-
return userLine(event, seq, options);
|
|
124
|
-
}
|
|
125
|
-
if (event.type === 'assistant/message') {
|
|
126
|
-
if (options.keepAssistant === false)
|
|
127
|
-
return undefined;
|
|
128
|
-
return assistantLine(event, seq, options);
|
|
129
|
-
}
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
/** A user message is kept whole unless it is long, then clipped head and tail. */
|
|
133
|
-
function userLine(event, seq, options) {
|
|
134
|
-
const text = messageText(event).replace(/\s+/g, ' ').trim();
|
|
135
|
-
if (text.length === 0)
|
|
136
|
-
return undefined;
|
|
137
|
-
const source = `session/<id>/${LAYER_CONSTRAINT}`;
|
|
138
|
-
if (text.length <= USER_CLIP_AT) {
|
|
139
|
-
return { kind: 'user', seq, text: `## [user ${seq}]\n${text}`, sourceChars: text.length };
|
|
140
|
-
}
|
|
141
|
-
const head = text.slice(0, USER_CLIP_KEEP);
|
|
142
|
-
const tail = text.slice(-USER_CLIP_KEEP);
|
|
143
|
-
const dropped = text.length - head.length - tail.length;
|
|
144
|
-
return {
|
|
145
|
-
kind: 'user',
|
|
146
|
-
seq,
|
|
147
|
-
sourceChars: text.length,
|
|
148
|
-
text: [
|
|
149
|
-
`## [user ${seq}]`,
|
|
150
|
-
head,
|
|
151
|
-
clipNotice(`${text.length}`, `${dropped}`, options.base, LAYER_CONSTRAINT, seq),
|
|
152
|
-
tail,
|
|
153
|
-
].join('\n'),
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
/** An assistant message keeps its head and tail; the middle stays in the archive. */
|
|
157
|
-
function assistantLine(event, seq, options) {
|
|
158
|
-
const text = messageText(event).replace(/\s+/g, ' ').trim();
|
|
159
|
-
if (text.length === 0)
|
|
160
|
-
return undefined;
|
|
161
|
-
if (text.length <= ASSISTANT_MIN_SPLIT) {
|
|
162
|
-
return { kind: 'assistant', seq, text: `## [assistant ${seq}]\n${text}`, sourceChars: text.length };
|
|
163
|
-
}
|
|
164
|
-
const head = text.slice(0, ASSISTANT_KEEP);
|
|
165
|
-
const tail = text.slice(-ASSISTANT_KEEP);
|
|
166
|
-
const dropped = text.length - head.length - tail.length;
|
|
167
|
-
return {
|
|
168
|
-
kind: 'assistant',
|
|
169
|
-
seq,
|
|
170
|
-
sourceChars: text.length,
|
|
171
|
-
text: [
|
|
172
|
-
`## [assistant ${seq}]`,
|
|
173
|
-
head,
|
|
174
|
-
clipNotice(`${text.length}`, `${dropped}`, options.base, LAYER_FINDING, seq),
|
|
175
|
-
tail,
|
|
176
|
-
].join('\n'),
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
/** Tool output is never copied: one line names the call and where its output lives. */
|
|
180
|
-
function toolLine(event, seq, options, names) {
|
|
181
|
-
const data = asRecord(event.data);
|
|
182
|
-
const message = asRecord(data?.message) ?? data;
|
|
183
|
-
const callId = firstCallId(message?.content);
|
|
184
|
-
const name = toolNameFor(data, callId, names);
|
|
185
|
-
const chars = messageText(event).length;
|
|
186
|
-
return {
|
|
187
|
-
kind: 'tool',
|
|
188
|
-
seq,
|
|
189
|
-
sourceChars: chars,
|
|
190
|
-
text: `## [tool ${name} seq ${seq}, ${chars} chars \u2192 search \`${options.base}/${LAYER_FINDING}\`]`,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* The archive pointer left where content was clipped.
|
|
195
|
-
*
|
|
196
|
-
* @param total - characters in the source event.
|
|
197
|
-
* @param dropped - characters not carried into the transcript.
|
|
198
|
-
* @param base - archive source root.
|
|
199
|
-
* @param layer - archive layer holding the original.
|
|
200
|
-
* @param seq - the source event's sequence number.
|
|
201
|
-
* @returns the model-facing notice.
|
|
202
|
-
*/
|
|
203
|
-
export function clipNotice(total, dropped, base, layer, seq) {
|
|
204
|
-
return (`[... ${dropped} of ${total} chars elided from seq ${seq}; ` +
|
|
205
|
-
`retrieve with ctx_search(source: "${base}/${layer}") ...]`);
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* Render transcript lines into one text body, stopping at the size bound.
|
|
209
|
-
*
|
|
210
|
-
* The bound is enforced from the end: the newest entries matter most to a
|
|
211
|
-
* resuming model, so an over-long transcript keeps its tail and reports how
|
|
212
|
-
* many older entries were dropped.
|
|
213
|
-
*
|
|
214
|
-
* @param lines - transcript lines in session order.
|
|
215
|
-
* @param maxChars - upper bound on the rendered body.
|
|
216
|
-
* @returns the rendered body.
|
|
217
|
-
*/
|
|
218
|
-
export function renderTranscript(lines, maxChars) {
|
|
219
|
-
const rendered = lines.map(line => line.text);
|
|
220
|
-
let total = rendered.reduce((sum, text) => sum + text.length + 2, 0);
|
|
221
|
-
if (total <= maxChars)
|
|
222
|
-
return rendered.join('\n\n');
|
|
223
|
-
// Drop from the oldest until the body fits, then say how many were dropped.
|
|
224
|
-
let first = 0;
|
|
225
|
-
while (first < rendered.length && total > maxChars) {
|
|
226
|
-
total -= rendered[first].length + 2;
|
|
227
|
-
first += 1;
|
|
228
|
-
}
|
|
229
|
-
const droppedCount = first;
|
|
230
|
-
const notice = `[... ${droppedCount} older transcript entries elided; the archived span is searchable via the archive index below ...]`;
|
|
231
|
-
return [notice, ...rendered.slice(first)].join('\n\n');
|
|
232
|
-
}
|
|
233
|
-
/** Text of a message-shaped event, reading nested tool-result content too. */
|
|
234
|
-
function messageText(event) {
|
|
235
|
-
const data = asRecord(event.data);
|
|
236
|
-
if (data === undefined)
|
|
237
|
-
return '';
|
|
238
|
-
if (event.type === 'tool/call') {
|
|
239
|
-
const name = typeof data.name === 'string' ? data.name : '';
|
|
240
|
-
const args = typeof data.arguments === 'string' ? data.arguments : '';
|
|
241
|
-
return name.length === 0 ? '' : `${name} ${args}`.trim();
|
|
242
|
-
}
|
|
243
|
-
const message = asRecord(data.message) ?? data;
|
|
244
|
-
return blocksToText(message?.content);
|
|
245
|
-
}
|
|
246
|
-
/** Flatten a content value (string, block array, or tool-result envelope) to text. */
|
|
247
|
-
function blocksToText(content) {
|
|
248
|
-
if (typeof content === 'string')
|
|
249
|
-
return content;
|
|
250
|
-
if (!Array.isArray(content))
|
|
251
|
-
return '';
|
|
252
|
-
const parts = [];
|
|
253
|
-
for (const block of content) {
|
|
254
|
-
if (block === null || typeof block !== 'object')
|
|
255
|
-
continue;
|
|
256
|
-
const record = block;
|
|
257
|
-
if (record.type === 'text' && typeof record.text === 'string') {
|
|
258
|
-
parts.push(record.text);
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
// tool/result wraps its payload one level down.
|
|
262
|
-
if (Array.isArray(record.content)) {
|
|
263
|
-
const inner = blocksToText(record.content);
|
|
264
|
-
if (inner.length > 0)
|
|
265
|
-
parts.push(inner);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return parts.join('\n');
|
|
269
|
-
}
|
|
270
|
-
/** First tool-call id found in a tool-result content value. */
|
|
271
|
-
function firstCallId(content) {
|
|
272
|
-
if (!Array.isArray(content))
|
|
273
|
-
return undefined;
|
|
274
|
-
for (const block of content) {
|
|
275
|
-
if (block === null || typeof block !== 'object')
|
|
276
|
-
continue;
|
|
277
|
-
const record = block;
|
|
278
|
-
if (typeof record.toolCallId === 'string')
|
|
279
|
-
return record.toolCallId;
|
|
280
|
-
}
|
|
281
|
-
return undefined;
|
|
282
|
-
}
|
|
283
|
-
/** The tool name a result belongs to, resolved from its paired call. */
|
|
284
|
-
function toolNameFor(data, callId, names) {
|
|
285
|
-
if (data !== undefined && typeof data.name === 'string' && data.name.length > 0)
|
|
286
|
-
return data.name;
|
|
287
|
-
if (callId === undefined)
|
|
288
|
-
return 'tool';
|
|
289
|
-
const paired = names.get(callId);
|
|
290
|
-
if (paired !== undefined)
|
|
291
|
-
return paired;
|
|
292
|
-
// The call may sit outside the compacted span; name the result by its leading
|
|
293
|
-
// id segment rather than silently labelling every such line "tool".
|
|
294
|
-
const head = callId.split('|')[0];
|
|
295
|
-
return head.length > 0 ? head : 'tool';
|
|
296
|
-
}
|
|
297
|
-
/** Whether a user message is harness-injected context rather than a real turn. */
|
|
298
|
-
function isInjected(event) {
|
|
299
|
-
const text = messageText(event).trimStart();
|
|
300
|
-
return (text.startsWith('<current_runtime_context') ||
|
|
301
|
-
text.startsWith('<active_memory') ||
|
|
302
|
-
text.startsWith('<system-reminder') ||
|
|
303
|
-
text.startsWith('<resume_snapshot') ||
|
|
304
|
-
text.startsWith('Current runtime context') ||
|
|
305
|
-
text.startsWith('The available skill catalog changed'));
|
|
306
|
-
}
|
|
307
|
-
/** The `source` record of a message-shaped event, when present. */
|
|
308
|
-
function sourceOf(event) {
|
|
309
|
-
const data = asRecord(event.data);
|
|
310
|
-
if (data === undefined)
|
|
311
|
-
return undefined;
|
|
312
|
-
const message = asRecord(data.message);
|
|
313
|
-
const source = asRecord(message?.source) ?? asRecord(data.source);
|
|
314
|
-
if (source === undefined)
|
|
315
|
-
return undefined;
|
|
316
|
-
return {
|
|
317
|
-
kind: typeof source.kind === 'string' ? source.kind : undefined,
|
|
318
|
-
plugin: typeof source.plugin === 'string' ? source.plugin : undefined,
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
function asRecord(value) {
|
|
322
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
323
|
-
? value
|
|
324
|
-
: undefined;
|
|
325
|
-
}
|