ds4-context-engine 0.2.0 → 0.3.0-alpha.1
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 +21 -3
- package/docs/ADR/README.md +5 -0
- package/docs/ARCHITECTURE.md +9 -1
- package/docs/CONTEXT_PERSISTENCE_TOOL.md +126 -0
- package/docs/MEMORY_AND_PINS.md +3 -3
- package/docs/PRIVACY.md +9 -1
- package/docs/RELEASING.md +15 -5
- package/docs/STORAGE.md +2 -2
- package/docs/releases/0.3.0-alpha.1.md +83 -0
- package/package.json +3 -2
- package/src/extension/context-persistence-contract.ts +150 -0
- package/src/extension/context-persistence-egress.ts +223 -0
- package/src/extension/context-persistence-result.ts +450 -0
- package/src/extension/context-persistence-tool.ts +1948 -0
- package/src/extension/index.ts +2 -0
- package/src/extension/runtime.ts +355 -22
- package/src/pi-adapter/version.ts +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { StringEnum, Type, type Static } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
export const CONTEXT_PERSISTENCE_TOOL_CONTRACT = "ds4-context-persistence-tool-v1" as const;
|
|
4
|
+
export const CONTEXT_PERSISTENCE_RESULT_CONTRACT = "ds4-context-persistence-result-v1" as const;
|
|
5
|
+
export const CONTEXT_PERSISTENCE_TOOL_NAME = "context_persistence" as const;
|
|
6
|
+
export const CONTEXT_PERSISTENCE_DESCRIPTION = "Inspect DS4 Pins/Memory. Write only after an explicit user request; writes require local user confirmation." as const;
|
|
7
|
+
export const CONTEXT_PERSISTENCE_PROMPT_SNIPPET = "Inspect or manage user-confirmed DS4 pins and durable memory" as const;
|
|
8
|
+
export const CONTEXT_PERSISTENCE_PROMPT_GUIDELINES = [
|
|
9
|
+
"Use context_persistence only to inspect DS4 persistent state or when the user explicitly requests a persistence mutation.",
|
|
10
|
+
"After an explicit persistence request, call the write action directly; context_persistence itself obtains the required local UI confirmation, so do not ask for separate confirmation in chat.",
|
|
11
|
+
"Never create a pin or memory merely because information appears useful.",
|
|
12
|
+
"Use pins for confirmed constraints or instructions that must remain prominent. Use memory for durable facts, decisions, and historical knowledge.",
|
|
13
|
+
"Default new persistence to session scope. Use project or branch scope only when explicitly requested or unambiguous; durable Memory does not support branch scope.",
|
|
14
|
+
"Before superseding, removing, invalidating, expiring, excluding, or including, read first and use the exact ID or reference plus targetRevision; never mutate from a fuzzy query.",
|
|
15
|
+
"Do not claim local-only protection if a remote model already saw the input; use /context or a local provider for data that must never reach a remote provider.",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export const CONTEXT_PERSISTENCE_ACTIONS = [
|
|
19
|
+
"pins_list",
|
|
20
|
+
"pins_find",
|
|
21
|
+
"pin_add",
|
|
22
|
+
"pin_supersede",
|
|
23
|
+
"pin_unpin",
|
|
24
|
+
"memory_list",
|
|
25
|
+
"memory_find",
|
|
26
|
+
"memory_add",
|
|
27
|
+
"memory_supersede",
|
|
28
|
+
"memory_invalidate",
|
|
29
|
+
"memory_expire",
|
|
30
|
+
"memory_sources",
|
|
31
|
+
"memory_source_exclude",
|
|
32
|
+
"memory_source_include",
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
export type ContextPersistenceAction = typeof CONTEXT_PERSISTENCE_ACTIONS[number];
|
|
36
|
+
|
|
37
|
+
export const CONTEXT_PERSISTENCE_READ_ACTIONS = [
|
|
38
|
+
"pins_list",
|
|
39
|
+
"pins_find",
|
|
40
|
+
"memory_list",
|
|
41
|
+
"memory_find",
|
|
42
|
+
"memory_sources",
|
|
43
|
+
] as const satisfies readonly ContextPersistenceAction[];
|
|
44
|
+
|
|
45
|
+
export type ContextPersistenceReadAction = typeof CONTEXT_PERSISTENCE_READ_ACTIONS[number];
|
|
46
|
+
|
|
47
|
+
export const CONTEXT_PERSISTENCE_WRITE_ACTIONS = CONTEXT_PERSISTENCE_ACTIONS.filter(
|
|
48
|
+
(action): action is Exclude<ContextPersistenceAction, ContextPersistenceReadAction> =>
|
|
49
|
+
!(CONTEXT_PERSISTENCE_READ_ACTIONS as readonly string[]).includes(action),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
export const CONTEXT_PERSISTENCE_PARAMS = Type.Object({
|
|
53
|
+
action: StringEnum(CONTEXT_PERSISTENCE_ACTIONS),
|
|
54
|
+
scope: Type.Optional(StringEnum(["session", "branch", "project"] as const, {
|
|
55
|
+
description: "Add only; Memory excludes branch",
|
|
56
|
+
})),
|
|
57
|
+
id: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),
|
|
58
|
+
targetRevision: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),
|
|
59
|
+
content: Type.Optional(Type.String({ minLength: 1, maxLength: 20_000 })),
|
|
60
|
+
key: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
|
|
61
|
+
query: Type.Optional(Type.String({ minLength: 2, maxLength: 200 })),
|
|
62
|
+
reason: Type.Optional(Type.String({
|
|
63
|
+
minLength: 1,
|
|
64
|
+
maxLength: 500,
|
|
65
|
+
description: "Lifecycle or source exclusion only",
|
|
66
|
+
})),
|
|
67
|
+
classification: Type.Optional(StringEnum([
|
|
68
|
+
"normal",
|
|
69
|
+
"internal",
|
|
70
|
+
"sensitive",
|
|
71
|
+
"local-only",
|
|
72
|
+
] as const)),
|
|
73
|
+
activeOnly: Type.Optional(Type.Boolean()),
|
|
74
|
+
maxResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),
|
|
75
|
+
}, { additionalProperties: false });
|
|
76
|
+
|
|
77
|
+
export type ContextPersistenceParams = Static<typeof CONTEXT_PERSISTENCE_PARAMS>;
|
|
78
|
+
|
|
79
|
+
const ALLOWED_FIELDS = {
|
|
80
|
+
pins_list: ["action", "activeOnly", "maxResults"],
|
|
81
|
+
pins_find: ["action", "query", "activeOnly", "maxResults"],
|
|
82
|
+
pin_add: ["action", "content", "scope", "classification"],
|
|
83
|
+
pin_supersede: ["action", "id", "targetRevision", "content", "classification"],
|
|
84
|
+
pin_unpin: ["action", "id", "targetRevision", "reason"],
|
|
85
|
+
memory_list: ["action", "activeOnly", "maxResults"],
|
|
86
|
+
memory_find: ["action", "query", "activeOnly", "maxResults"],
|
|
87
|
+
memory_add: ["action", "content", "scope", "key", "classification"],
|
|
88
|
+
memory_supersede: ["action", "id", "targetRevision", "content", "classification"],
|
|
89
|
+
memory_invalidate: ["action", "id", "targetRevision", "reason"],
|
|
90
|
+
memory_expire: ["action", "id", "targetRevision", "reason"],
|
|
91
|
+
memory_sources: ["action", "maxResults"],
|
|
92
|
+
memory_source_exclude: ["action", "id", "targetRevision", "reason"],
|
|
93
|
+
memory_source_include: ["action", "id", "targetRevision"],
|
|
94
|
+
} as const satisfies Record<ContextPersistenceAction, readonly (keyof ContextPersistenceParams)[]>;
|
|
95
|
+
|
|
96
|
+
const REQUIRED_FIELDS = {
|
|
97
|
+
pins_list: [],
|
|
98
|
+
pins_find: ["query"],
|
|
99
|
+
pin_add: ["content"],
|
|
100
|
+
pin_supersede: ["id", "targetRevision", "content"],
|
|
101
|
+
pin_unpin: ["id", "targetRevision"],
|
|
102
|
+
memory_list: [],
|
|
103
|
+
memory_find: ["query"],
|
|
104
|
+
memory_add: ["content"],
|
|
105
|
+
memory_supersede: ["id", "targetRevision", "content"],
|
|
106
|
+
memory_invalidate: ["id", "targetRevision"],
|
|
107
|
+
memory_expire: ["id", "targetRevision"],
|
|
108
|
+
memory_sources: [],
|
|
109
|
+
memory_source_exclude: ["id", "targetRevision"],
|
|
110
|
+
memory_source_include: ["id", "targetRevision"],
|
|
111
|
+
} as const satisfies Record<ContextPersistenceAction, readonly (keyof ContextPersistenceParams)[]>;
|
|
112
|
+
|
|
113
|
+
export type ContextPersistenceValidationCode =
|
|
114
|
+
| "invalid-parameters"
|
|
115
|
+
| "invalid-scope";
|
|
116
|
+
|
|
117
|
+
export type ContextPersistenceValidation =
|
|
118
|
+
| { ok: true; value: ContextPersistenceParams }
|
|
119
|
+
| { ok: false; errorCode: ContextPersistenceValidationCode };
|
|
120
|
+
|
|
121
|
+
function isAction(value: unknown): value is ContextPersistenceAction {
|
|
122
|
+
return typeof value === "string"
|
|
123
|
+
&& (CONTEXT_PERSISTENCE_ACTIONS as readonly string[]).includes(value);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isReadAction(action: ContextPersistenceAction): action is ContextPersistenceReadAction {
|
|
127
|
+
return (CONTEXT_PERSISTENCE_READ_ACTIONS as readonly string[]).includes(action);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Action-specific validation performed after TypeBox transport validation.
|
|
132
|
+
* Values are never included in failures so provider-visible errors stay metadata-only.
|
|
133
|
+
*/
|
|
134
|
+
export function validateContextPersistenceParams(
|
|
135
|
+
params: ContextPersistenceParams,
|
|
136
|
+
): ContextPersistenceValidation {
|
|
137
|
+
if (!params || !isAction(params.action)) return { ok: false, errorCode: "invalid-parameters" };
|
|
138
|
+
const keys = Object.keys(params) as (keyof ContextPersistenceParams)[];
|
|
139
|
+
const allowed = new Set<keyof ContextPersistenceParams>(ALLOWED_FIELDS[params.action]);
|
|
140
|
+
if (keys.some((key) => !allowed.has(key))) {
|
|
141
|
+
return { ok: false, errorCode: "invalid-parameters" };
|
|
142
|
+
}
|
|
143
|
+
if (REQUIRED_FIELDS[params.action].some((key) => params[key] === undefined)) {
|
|
144
|
+
return { ok: false, errorCode: "invalid-parameters" };
|
|
145
|
+
}
|
|
146
|
+
if (params.scope === "branch" && params.action === "memory_add") {
|
|
147
|
+
return { ok: false, errorCode: "invalid-scope" };
|
|
148
|
+
}
|
|
149
|
+
return { ok: true, value: params };
|
|
150
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONTEXT_PERSISTENCE_ACTIONS,
|
|
3
|
+
CONTEXT_PERSISTENCE_TOOL_NAME,
|
|
4
|
+
} from "./context-persistence-contract.ts";
|
|
5
|
+
import {
|
|
6
|
+
renderHistoricalContextPersistenceResult,
|
|
7
|
+
sanitizeHistoricalContextPersistenceDetails,
|
|
8
|
+
} from "./context-persistence-result.ts";
|
|
9
|
+
|
|
10
|
+
export const CONTEXT_PERSISTENCE_EGRESS_SENTINEL = "[omitted-by-ds4-egress-policy]" as const;
|
|
11
|
+
|
|
12
|
+
const ACTIONS = new Set<string>(CONTEXT_PERSISTENCE_ACTIONS);
|
|
13
|
+
const SAFE_ARGUMENT_KEYS = new Set([
|
|
14
|
+
"action",
|
|
15
|
+
"scope",
|
|
16
|
+
"id",
|
|
17
|
+
"targetRevision",
|
|
18
|
+
"classification",
|
|
19
|
+
"activeOnly",
|
|
20
|
+
"maxResults",
|
|
21
|
+
]);
|
|
22
|
+
const SENSITIVE_ARGUMENT_KEYS = new Set(["content", "query", "key", "reason"]);
|
|
23
|
+
const OPAQUE_ID = /^(?:source_[A-Za-z0-9_-]{22,57}|[A-Za-z0-9._:-]{1,128})$/u;
|
|
24
|
+
const REVISION = /^rev_[A-Za-z0-9_-]{1,60}$/u;
|
|
25
|
+
const SAFE_ENUM = /^(?:session|branch|project|normal|internal|sensitive|local-only)$/u;
|
|
26
|
+
const SAFE_METADATA_LINE = /^(?:pin|memory|project-memory-source) (?:source_[A-Za-z0-9_-]{22,57}|[A-Za-z0-9._:-]{1,128})(?:; (?:scope|status|classification|applicable|createdAt|updatedAt|revision|match|score|indexedMutations|activeProjectMemories|activeProjectPins|hasMalformedLines|error)=[A-Za-z0-9_-]+)+$/u;
|
|
27
|
+
const SAFE_SUMMARY_LINE = /^(?:pins_list|pins_find|memory_list|memory_find|memory_sources): \d{1,3} (?:item\(s\)|match\(es\)); truncated=(?:true|false); incomplete=(?:true|false)\.$/u;
|
|
28
|
+
const PREVIEW_LINE = /^(pin|memory) ([\x21-\x7e]{1,128}): /u;
|
|
29
|
+
|
|
30
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
31
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function safeArgumentValue(key: string, value: unknown): unknown {
|
|
35
|
+
if (key === "action") return typeof value === "string" && ACTIONS.has(value)
|
|
36
|
+
? value
|
|
37
|
+
: CONTEXT_PERSISTENCE_EGRESS_SENTINEL;
|
|
38
|
+
if (key === "activeOnly") return typeof value === "boolean"
|
|
39
|
+
? value
|
|
40
|
+
: CONTEXT_PERSISTENCE_EGRESS_SENTINEL;
|
|
41
|
+
if (key === "maxResults") return Number.isInteger(value) && Number(value) >= 1 && Number(value) <= 100
|
|
42
|
+
? value
|
|
43
|
+
: CONTEXT_PERSISTENCE_EGRESS_SENTINEL;
|
|
44
|
+
if (key === "scope" || key === "classification") {
|
|
45
|
+
return typeof value === "string" && SAFE_ENUM.test(value)
|
|
46
|
+
? value
|
|
47
|
+
: CONTEXT_PERSISTENCE_EGRESS_SENTINEL;
|
|
48
|
+
}
|
|
49
|
+
if (key === "id") {
|
|
50
|
+
return typeof value === "string" && OPAQUE_ID.test(value)
|
|
51
|
+
? value
|
|
52
|
+
: CONTEXT_PERSISTENCE_EGRESS_SENTINEL;
|
|
53
|
+
}
|
|
54
|
+
if (key === "targetRevision") {
|
|
55
|
+
return typeof value === "string" && REVISION.test(value)
|
|
56
|
+
? value
|
|
57
|
+
: CONTEXT_PERSISTENCE_EGRESS_SENTINEL;
|
|
58
|
+
}
|
|
59
|
+
return CONTEXT_PERSISTENCE_EGRESS_SENTINEL;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Arguments are reduced without retaining raw content in temporary result objects. */
|
|
63
|
+
export function sanitizeHistoricalContextPersistenceArguments(value: unknown): Record<string, unknown> {
|
|
64
|
+
if (!isRecord(value)) return {};
|
|
65
|
+
const sanitized: Record<string, unknown> = {};
|
|
66
|
+
for (const [key, candidate] of Object.entries(value)) {
|
|
67
|
+
if (SAFE_ARGUMENT_KEYS.has(key)) sanitized[key] = safeArgumentValue(key, candidate);
|
|
68
|
+
else if (SENSITIVE_ARGUMENT_KEYS.has(key)) sanitized[key] = CONTEXT_PERSISTENCE_EGRESS_SENTINEL;
|
|
69
|
+
else sanitized[key] = CONTEXT_PERSISTENCE_EGRESS_SENTINEL;
|
|
70
|
+
}
|
|
71
|
+
return sanitized;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sanitizeArgumentContainer(value: unknown): unknown {
|
|
75
|
+
if (typeof value === "string") {
|
|
76
|
+
try {
|
|
77
|
+
return JSON.stringify(sanitizeHistoricalContextPersistenceArguments(JSON.parse(value)));
|
|
78
|
+
} catch {
|
|
79
|
+
return JSON.stringify({ omitted: CONTEXT_PERSISTENCE_EGRESS_SENTINEL });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return sanitizeHistoricalContextPersistenceArguments(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Preserves only the fixed metadata grammar emitted by the V1 result renderer. */
|
|
86
|
+
export function sanitizeHistoricalContextPersistenceText(value: unknown): string {
|
|
87
|
+
if (typeof value !== "string" || value.length > 96 * 1024) return CONTEXT_PERSISTENCE_EGRESS_SENTINEL;
|
|
88
|
+
const output: string[] = [];
|
|
89
|
+
for (const line of value.split("\n").slice(0, 205)) {
|
|
90
|
+
if (SAFE_SUMMARY_LINE.test(line)
|
|
91
|
+
|| SAFE_METADATA_LINE.test(line)
|
|
92
|
+
|| line === "Search incomplete; refine the query or use an exact opaque ID.") {
|
|
93
|
+
output.push(line);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const preview = PREVIEW_LINE.exec(line);
|
|
97
|
+
if (preview) {
|
|
98
|
+
output.push(`${preview[1]} ${preview[2]}: preview omitted by policy`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (/^(?:pins_|pin_|memory_)[a-z_]+ (?:ok|rejected|cancelled|unavailable|committed|committed_projection_pending|indeterminate)(?:: [a-z0-9-]{1,64})?\.$/u.test(line)) {
|
|
102
|
+
output.push(line);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
output.push(CONTEXT_PERSISTENCE_EGRESS_SENTINEL);
|
|
106
|
+
}
|
|
107
|
+
return output.join("\n") || CONTEXT_PERSISTENCE_EGRESS_SENTINEL;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function toolName(record: Record<string, unknown>): string | undefined {
|
|
111
|
+
if (typeof record.name === "string") return record.name;
|
|
112
|
+
if (typeof record.toolName === "string") return record.toolName;
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function identifiers(record: Record<string, unknown>): string[] {
|
|
117
|
+
return [
|
|
118
|
+
record.id,
|
|
119
|
+
record.call_id,
|
|
120
|
+
record.toolCallId,
|
|
121
|
+
record.tool_call_id,
|
|
122
|
+
record.toolUseId,
|
|
123
|
+
record.tool_use_id,
|
|
124
|
+
].filter((value): value is string => typeof value === "string");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function collectToolCallIds(value: unknown, ids: Set<string>, seen: WeakSet<object>): void {
|
|
128
|
+
if (!value || typeof value !== "object" || seen.has(value)) return;
|
|
129
|
+
seen.add(value);
|
|
130
|
+
if (Array.isArray(value)) {
|
|
131
|
+
for (const child of value) collectToolCallIds(child, ids, seen);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const record = value as Record<string, unknown>;
|
|
135
|
+
if (toolName(record) === CONTEXT_PERSISTENCE_TOOL_NAME) {
|
|
136
|
+
for (const id of identifiers(record)) ids.add(id);
|
|
137
|
+
}
|
|
138
|
+
for (const child of Object.values(record)) collectToolCallIds(child, ids, seen);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function sanitizeContent(value: unknown, safeText: string): unknown {
|
|
142
|
+
if (typeof value === "string") return safeText;
|
|
143
|
+
if (!Array.isArray(value)) return [{ type: "text", text: safeText }];
|
|
144
|
+
return [{ type: "text", text: safeText }];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function sanitizeNode(
|
|
148
|
+
value: unknown,
|
|
149
|
+
toolCallIds: ReadonlySet<string>,
|
|
150
|
+
seen: WeakMap<object, unknown>,
|
|
151
|
+
): { value: unknown; changed: boolean } {
|
|
152
|
+
if (!value || typeof value !== "object") return { value, changed: false };
|
|
153
|
+
const prior = seen.get(value);
|
|
154
|
+
if (prior !== undefined) return { value: prior, changed: false };
|
|
155
|
+
if (Array.isArray(value)) {
|
|
156
|
+
const output: unknown[] = [];
|
|
157
|
+
seen.set(value, output);
|
|
158
|
+
let changed = false;
|
|
159
|
+
for (const child of value) {
|
|
160
|
+
const sanitized = sanitizeNode(child, toolCallIds, seen);
|
|
161
|
+
output.push(sanitized.value);
|
|
162
|
+
changed ||= sanitized.changed;
|
|
163
|
+
}
|
|
164
|
+
return changed ? { value: output, changed: true } : { value, changed: false };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const record = value as Record<string, unknown>;
|
|
168
|
+
const directToolCall = toolName(record) === CONTEXT_PERSISTENCE_TOOL_NAME;
|
|
169
|
+
const linkedResult = identifiers(record).some((id) => toolCallIds.has(id))
|
|
170
|
+
|| (record.role === "toolResult" && record.toolName === CONTEXT_PERSISTENCE_TOOL_NAME);
|
|
171
|
+
let changed = false;
|
|
172
|
+
const output: Record<string, unknown> = {};
|
|
173
|
+
seen.set(value, output);
|
|
174
|
+
|
|
175
|
+
for (const [key, child] of Object.entries(record)) {
|
|
176
|
+
if (directToolCall && (key === "arguments" || key === "args" || key === "input")) {
|
|
177
|
+
output[key] = sanitizeArgumentContainer(child);
|
|
178
|
+
changed = true;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (linkedResult && key === "details") {
|
|
182
|
+
const details = sanitizeHistoricalContextPersistenceDetails(child);
|
|
183
|
+
output[key] = details ?? {};
|
|
184
|
+
changed = true;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (linkedResult && (key === "content" || key === "output")) {
|
|
188
|
+
const details = sanitizeHistoricalContextPersistenceDetails(record.details);
|
|
189
|
+
const text = details
|
|
190
|
+
? renderHistoricalContextPersistenceResult(details)
|
|
191
|
+
: sanitizeHistoricalContextPersistenceText(
|
|
192
|
+
typeof child === "string"
|
|
193
|
+
? child
|
|
194
|
+
: Array.isArray(child)
|
|
195
|
+
? child.flatMap((block) => isRecord(block) && typeof block.text === "string" ? [block.text] : []).join("\n")
|
|
196
|
+
: undefined,
|
|
197
|
+
);
|
|
198
|
+
output[key] = sanitizeContent(child, text);
|
|
199
|
+
changed = true;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (linkedResult && key === "response") {
|
|
203
|
+
const response = isRecord(child) ? child : {};
|
|
204
|
+
const responseKey = "error" in response ? "error" : "output";
|
|
205
|
+
output[key] = {
|
|
206
|
+
[responseKey]: sanitizeHistoricalContextPersistenceText(response[responseKey]),
|
|
207
|
+
};
|
|
208
|
+
changed = true;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const sanitized = sanitizeNode(child, toolCallIds, seen);
|
|
212
|
+
output[key] = sanitized.value;
|
|
213
|
+
changed ||= sanitized.changed;
|
|
214
|
+
}
|
|
215
|
+
return changed ? { value: output, changed: true } : { value, changed: false };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function sanitizeContextPersistenceHistory<T>(value: T): { value: T; changed: boolean } {
|
|
219
|
+
const ids = new Set<string>();
|
|
220
|
+
collectToolCallIds(value, ids, new WeakSet());
|
|
221
|
+
const sanitized = sanitizeNode(value, ids, new WeakMap());
|
|
222
|
+
return { value: sanitized.value as T, changed: sanitized.changed };
|
|
223
|
+
}
|