odai-dsh-agent 0.0.7 → 0.0.9
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
CHANGED
|
@@ -13,7 +13,7 @@ $DSH_HOME/.agent-presets/odai/
|
|
|
13
13
|
.odai-agent.json
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
The composition preserves every capability row and setting from the `standard` agent surface in `@deepseek-ai/dsh@0.1.0-rc.6`, then adds the scoped Odai runtime in default `auto` mode. Release verification compares the complete derived composition against that exact DSH Standard source; a missing, added, reordered, or changed Standard row fails the probe. Host-owned persistence, sandbox, approval, registries, and base controller selection remain in the selected DSH profile. Ordinary requests stay on that controller. A complete contextual high-impact decision gap upgrades the same turn only when a planner mapping exists; the controller's durable `request/header` proves the actual route. Explicit independent planning/review requests still start a verified child when their mappings exist. `execute` remains an explicit all-delegation comparison mode. `observe` changes no model and starts no child; it injects a local evidence-and-decision protocol and makes an unresolved high-impact turn read-only. For DSH compaction
|
|
16
|
+
The composition preserves every capability row and setting from the `standard` agent surface in `@deepseek-ai/dsh@0.1.0-rc.6`, then adds the scoped Odai runtime in default `auto` mode. Release verification compares the complete derived composition against that exact DSH Standard source; a missing, added, reordered, or changed Standard row fails the probe. Host-owned persistence, sandbox, approval, registries, and base controller selection remain in the selected DSH profile. Ordinary requests stay on that controller. A complete contextual high-impact decision gap upgrades the same turn only when a planner mapping exists; the controller's durable `request/header` proves the actual route. Explicit independent planning/review requests still start a verified child when their mappings exist. `execute` remains an explicit all-delegation comparison mode. `observe` changes no model and starts no child; it injects a local evidence-and-decision protocol and makes an unresolved high-impact turn read-only. For DSH compaction, an explicit user-selected summary reasoning effort overrides only that auxiliary call. When omitted, same-provider/model compaction inherits the session's current reasoning effort for cache compatibility; a cross-model target removes an effort matching that durable route, while a distinct preselected effort is preserved because the request envelope exposes no stronger provenance.
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
@@ -67,7 +67,13 @@ Natural requests include `use normal output`, `use soft concise output`, `enable
|
|
|
67
67
|
|
|
68
68
|
An economy ceiling only tightens an existing lower host request value and is not a locally enforceable hard billing boundary. A provider may count hidden reasoning inside it, exceed or ignore it, or end before useful final text, especially at a high reasoning effort; strict compliance must be checked from per-request usage. Odai enables economy only when requested and never invents a non-default custom value. The mode does not alter child-agent role budgets, compaction, checkpoints, or other internal context; an incomplete token-capped compaction fails closed instead of replacing history.
|
|
69
69
|
|
|
70
|
-
A same-provider/model compaction
|
|
70
|
+
A same-provider/model compaction inherits controller reasoning while keeping its independent summary budget. Odai leaves prompt-cache retention unset by default; `ODAI_COMPACTION_CACHE_RETENTION` can explicitly select `short`, `long`, or `none`. `provider-default` means Odai adds no retention, while any explicit incoming retention remains authoritative; configured retention still applies when host routing has already supplied reasoning. Custom preset compositions can set the same value through runtime `compaction.cacheRetention`. The first controller request after a landed summary still rebuilds the changed summary prefix.
|
|
71
|
+
|
|
72
|
+
## Compaction model
|
|
73
|
+
|
|
74
|
+
The default compaction-summary model is `inherit`, which preserves the conversation's current provider/model behavior. A user can explicitly set a separate target and optional reasoning effort in natural language, such as `压缩模型用 provider-x/model-summary,推理档 high`; `odai_compaction_config` persists those explicit values in `$DSH_HOME/odai/compaction.json`. Removing the target restores inheritance. Agent and Plugin share the store when both are deliberately present.
|
|
75
|
+
|
|
76
|
+
The target applies only to future `compaction` summary requests. It does not change the controller, planner, executor, reviewer, ordinary conversation, summary output budget, or cache-retention policy. An explicitly configured `reasoningEffort` overrides reasoning only for those summaries. When omitted, existing behavior remains: same-route summaries can inherit controller reasoning, while a cross-model target removes only reasoning proven by equality with the durable controller route and preserves a distinct preselected effort. Each configured target receives one provider-neutral integrity suffix that keeps current facts above superseded/rejected history, preserves continuation-critical opaque values exactly, and self-checks contradictions; duplicate Agent/Plugin runtime instances add it only once. Odai never chooses the provider, model, or reasoning effort on the user's behalf. An invalid store is reported by the tool while runtime requests inherit safely until `set` or `remove` repairs it. An unavailable route, provider error, or incomplete summary fails closed: DSH retains the original history rather than replacing it with a partial checkpoint.
|
|
71
77
|
|
|
72
78
|
## Skill sources
|
|
73
79
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { dirname, resolve } from "node:path";
|
|
11
|
+
|
|
12
|
+
import { acquireOwnedStoreLock } from "./store-lock.mjs";
|
|
13
|
+
|
|
14
|
+
const STORE_SCHEMA_VERSION = 1;
|
|
15
|
+
const TARGET_FIELDS = new Set(["provider", "model", "reasoningEffort"]);
|
|
16
|
+
const STATE_PROTOCOL_SOURCE = Object.freeze({
|
|
17
|
+
kind: "plugin",
|
|
18
|
+
plugin: "odai-dsh-runtime",
|
|
19
|
+
form: "instructions",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const COMPACTION_STATE_PROTOCOL = [
|
|
23
|
+
"Additional checkpoint integrity protocol:",
|
|
24
|
+
"- Treat facts explicitly marked CURRENT, corrected, or authoritative as the active state.",
|
|
25
|
+
"- Facts marked SUPERSEDED, REJECTED, obsolete, or historical may be retained only as rejected history; never describe them as current, latest, selected, or pending.",
|
|
26
|
+
"- Preserve opaque identifiers, paths, commands, status values, delimiters, and numeric values byte-for-byte when they determine continuation state.",
|
|
27
|
+
"- Before emitting the checkpoint, self-check that no rejected fact is framed as active and that no active value conflicts with another section.",
|
|
28
|
+
].join("\n");
|
|
29
|
+
|
|
30
|
+
export const COMPACTION_CONFIG_PROMPT = [
|
|
31
|
+
"## Odai compaction model configuration",
|
|
32
|
+
"When the user naturally asks to inspect, set, change, or reset the model or reasoning effort used to summarize compacted context, use odai_compaction_config.",
|
|
33
|
+
"Set a compaction target only when the user explicitly supplies both provider and model. Pass reasoningEffort only when the user explicitly supplies it. Never infer or silently choose any value. If the user changes only the effort for an existing persisted target, inspect and preserve that exact provider/model pair.",
|
|
34
|
+
"The default is inherit: compaction uses the current conversation route. A configured target changes only compaction-summary requests; it does not change the controller, planner, executor, reviewer, normal conversation, summary output budget, or cache-retention policy. An optional explicit reasoning effort is likewise scoped only to compaction summaries; omitting it preserves the existing reasoning inheritance and cross-model isolation behavior.",
|
|
35
|
+
"Configured targets also receive a provider-neutral checkpoint integrity protocol that keeps current facts above superseded or rejected history and preserves continuation-critical opaque values exactly.",
|
|
36
|
+
"Do not ask the user to edit YAML, JSON, managed Agent files, or Plugin configuration. The tool owns persistence. Set/remove changes apply to the next compaction request.",
|
|
37
|
+
"An invalid persisted store is visible through the tool while runtime requests safely inherit the conversation route. If the configured route fails or produces an incomplete summary, DSH must preserve the original history rather than landing a partial checkpoint.",
|
|
38
|
+
].join("\n");
|
|
39
|
+
|
|
40
|
+
export class CompactionModelStoreValidationError extends Error {}
|
|
41
|
+
|
|
42
|
+
function assertPlainObject(value, field) {
|
|
43
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
44
|
+
throw new TypeError(`${field} must be an object`);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resolveCompactionTarget(value, field = "Odai compaction model target") {
|
|
50
|
+
const target = assertPlainObject(value, field);
|
|
51
|
+
const unknown = Object.keys(target).filter((name) => !TARGET_FIELDS.has(name));
|
|
52
|
+
if (unknown.length > 0) throw new TypeError(`${field} has unknown fields: ${unknown.join(", ")}`);
|
|
53
|
+
if (typeof target.provider !== "string" || target.provider.trim() === "") {
|
|
54
|
+
throw new TypeError(`${field}.provider must be a non-empty string`);
|
|
55
|
+
}
|
|
56
|
+
if (typeof target.model !== "string" || target.model.trim() === "") {
|
|
57
|
+
throw new TypeError(`${field}.model must be a non-empty string`);
|
|
58
|
+
}
|
|
59
|
+
if (target.reasoningEffort !== undefined
|
|
60
|
+
&& (typeof target.reasoningEffort !== "string" || target.reasoningEffort.trim() === "")) {
|
|
61
|
+
throw new TypeError(`${field}.reasoningEffort must be a non-empty string`);
|
|
62
|
+
}
|
|
63
|
+
return Object.freeze({
|
|
64
|
+
provider: target.provider.trim(),
|
|
65
|
+
model: target.model.trim(),
|
|
66
|
+
...(target.reasoningEffort === undefined ? {} : { reasoningEffort: target.reasoningEffort.trim() }),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolveCompactionConfigPath(configuredPath, env = process.env) {
|
|
71
|
+
if (configuredPath !== undefined) {
|
|
72
|
+
if (typeof configuredPath !== "string" || configuredPath.trim() === "") {
|
|
73
|
+
throw new TypeError("config.compaction.configPath must be a non-empty string");
|
|
74
|
+
}
|
|
75
|
+
return resolve(configuredPath.trim());
|
|
76
|
+
}
|
|
77
|
+
const dshHome = typeof env.DSH_HOME === "string" && env.DSH_HOME.trim() !== ""
|
|
78
|
+
? resolve(env.DSH_HOME.trim())
|
|
79
|
+
: resolve(homedir(), ".dsh");
|
|
80
|
+
return resolve(dshHome, "odai", "compaction.json");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function readCompactionModelStore(configPath) {
|
|
84
|
+
let text;
|
|
85
|
+
try {
|
|
86
|
+
text = readFileSync(configPath, "utf8");
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error?.code === "ENOENT") return Object.freeze({ schemaVersion: STORE_SCHEMA_VERSION });
|
|
89
|
+
throw new Error(`cannot read Odai compaction model configuration ${configPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
90
|
+
}
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(text);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw new CompactionModelStoreValidationError(`Odai compaction model configuration ${configPath} is not valid JSON`, { cause: error });
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const store = assertPlainObject(parsed, `Odai compaction model configuration ${configPath}`);
|
|
99
|
+
const unknown = Object.keys(store).filter((field) => !["schemaVersion", "target"].includes(field));
|
|
100
|
+
if (unknown.length > 0) {
|
|
101
|
+
throw new TypeError(`Odai compaction model configuration ${configPath} has unknown fields: ${unknown.join(", ")}`);
|
|
102
|
+
}
|
|
103
|
+
if (store.schemaVersion !== STORE_SCHEMA_VERSION) {
|
|
104
|
+
throw new TypeError(`Odai compaction model configuration ${configPath} has unsupported schemaVersion ${String(store.schemaVersion)}`);
|
|
105
|
+
}
|
|
106
|
+
if (store.target === undefined) return Object.freeze({ schemaVersion: STORE_SCHEMA_VERSION });
|
|
107
|
+
return Object.freeze({
|
|
108
|
+
schemaVersion: STORE_SCHEMA_VERSION,
|
|
109
|
+
target: resolveCompactionTarget(store.target, `Odai compaction model configuration ${configPath}.target`),
|
|
110
|
+
});
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error instanceof CompactionModelStoreValidationError) throw error;
|
|
113
|
+
throw new CompactionModelStoreValidationError(
|
|
114
|
+
`Odai compaction model configuration ${configPath} failed validation: ${error instanceof Error ? error.message : String(error)}`,
|
|
115
|
+
{ cause: error },
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function effectiveCompactionTarget(configPath) {
|
|
121
|
+
const stored = readCompactionModelStore(configPath);
|
|
122
|
+
return Object.freeze({
|
|
123
|
+
...(stored.target === undefined ? {} : { target: stored.target }),
|
|
124
|
+
source: stored.target === undefined ? "inherit" : "persisted",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function applyCompactionTarget(options, target, sessions) {
|
|
129
|
+
if (options?.purpose !== "compaction" || target === undefined) return false;
|
|
130
|
+
const currentRoute = options.sessionId === undefined
|
|
131
|
+
? undefined
|
|
132
|
+
: sessions?.get?.(options.sessionId)?.requestHeader?.()?.config;
|
|
133
|
+
const durableRouteKnown = typeof currentRoute?.provider === "string"
|
|
134
|
+
&& currentRoute.provider.length > 0
|
|
135
|
+
&& typeof currentRoute.model === "string"
|
|
136
|
+
&& currentRoute.model.length > 0;
|
|
137
|
+
const targetChangesDurableRoute = durableRouteKnown
|
|
138
|
+
&& (currentRoute.provider !== target.provider || currentRoute.model !== target.model);
|
|
139
|
+
const inheritedReasoning = targetChangesDurableRoute
|
|
140
|
+
&& typeof currentRoute.reasoningEffort === "string"
|
|
141
|
+
&& currentRoute.reasoningEffort.length > 0
|
|
142
|
+
&& options.reasoningEffort === currentRoute.reasoningEffort;
|
|
143
|
+
const changesRoute = options.provider !== target.provider || options.model !== target.model;
|
|
144
|
+
const changesReasoning = target.reasoningEffort !== undefined
|
|
145
|
+
&& options.reasoningEffort !== target.reasoningEffort;
|
|
146
|
+
if (!changesRoute && !inheritedReasoning && !changesReasoning) return false;
|
|
147
|
+
if (!Object.isExtensible(options)) {
|
|
148
|
+
throw new Error("configured Odai compaction model cannot be applied to an immutable request");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (target.reasoningEffort !== undefined) options.reasoningEffort = target.reasoningEffort;
|
|
152
|
+
else if (inheritedReasoning) delete options.reasoningEffort;
|
|
153
|
+
if (changesRoute) {
|
|
154
|
+
options.provider = target.provider;
|
|
155
|
+
options.model = target.model;
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function applyCompactionStateProtocol(options, target) {
|
|
161
|
+
if (options?.purpose !== "compaction" || target === undefined) return false;
|
|
162
|
+
if (!Array.isArray(options.messages) || !Object.isExtensible(options)) {
|
|
163
|
+
throw new Error("configured Odai compaction model requires a mutable message envelope for checkpoint integrity");
|
|
164
|
+
}
|
|
165
|
+
const alreadyApplied = options.messages.some((message) => (
|
|
166
|
+
message?.source?.kind === STATE_PROTOCOL_SOURCE.kind
|
|
167
|
+
&& message.source.plugin === STATE_PROTOCOL_SOURCE.plugin
|
|
168
|
+
&& message.source.form === STATE_PROTOCOL_SOURCE.form
|
|
169
|
+
&& Array.isArray(message.content)
|
|
170
|
+
&& message.content.some((block) => block?.type === "text" && block.text === COMPACTION_STATE_PROTOCOL)
|
|
171
|
+
));
|
|
172
|
+
if (alreadyApplied) return false;
|
|
173
|
+
options.messages = [...options.messages, {
|
|
174
|
+
id: randomUUID(),
|
|
175
|
+
role: "user",
|
|
176
|
+
content: [{ type: "text", text: COMPACTION_STATE_PROTOCOL }],
|
|
177
|
+
source: STATE_PROTOCOL_SOURCE,
|
|
178
|
+
}];
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function writeCompactionModelStore(configPath, target) {
|
|
183
|
+
mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
|
|
184
|
+
const temporary = `${configPath}.tmp-${process.pid}-${randomUUID()}`;
|
|
185
|
+
const value = `${JSON.stringify({ schemaVersion: STORE_SCHEMA_VERSION, target }, null, 2)}\n`;
|
|
186
|
+
try {
|
|
187
|
+
writeFileSync(temporary, value, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
188
|
+
renameSync(temporary, configPath);
|
|
189
|
+
} finally {
|
|
190
|
+
rmSync(temporary, { force: true });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function preserveInvalidStore(configPath) {
|
|
195
|
+
try {
|
|
196
|
+
renameSync(configPath, `${configPath}.invalid-${Date.now()}-${randomUUID()}`);
|
|
197
|
+
return true;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (error?.code === "ENOENT") return false;
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function resultFor(configPath, action, selection, recoveredInvalidStore = false) {
|
|
205
|
+
return {
|
|
206
|
+
action,
|
|
207
|
+
configPath,
|
|
208
|
+
source: selection.source,
|
|
209
|
+
...(selection.target === undefined ? {} : { target: selection.target }),
|
|
210
|
+
requiresNextCompaction: action !== "show",
|
|
211
|
+
...(selection.invalidStore ? { invalidStore: true } : {}),
|
|
212
|
+
...(recoveredInvalidStore ? { recoveredInvalidStore: true } : {}),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function createCompactionConfigTool(configPath, options = {}) {
|
|
217
|
+
const onConfigured = typeof options.onConfigured === "function" ? options.onConfigured : () => {};
|
|
218
|
+
const isChild = typeof options.isChild === "function"
|
|
219
|
+
? options.isChild
|
|
220
|
+
: (agent) => {
|
|
221
|
+
const header = agent?.session?.header;
|
|
222
|
+
return header?.origin === "subagent"
|
|
223
|
+
|| (Number.isSafeInteger(header?.delegationDepth) && header.delegationDepth > 0);
|
|
224
|
+
};
|
|
225
|
+
return {
|
|
226
|
+
name: "odai_compaction_config",
|
|
227
|
+
description: [
|
|
228
|
+
"Inspect, set, or remove the shared Odai compaction-summary target.",
|
|
229
|
+
"Use only when the user naturally asks about the compaction model/reasoning effort or explicitly supplies the provider and model to set. Never select a provider, model, or reasoning effort on the user's behalf.",
|
|
230
|
+
"The default is inherit. A persisted target changes only future compaction-summary requests; controller and responsibility routes, output budget, and cache retention stay unchanged. An optional user-specified reasoning effort applies only to those summaries.",
|
|
231
|
+
"Persisted targets also receive a provider-neutral integrity protocol that keeps current facts above superseded history and preserves continuation-critical opaque values exactly.",
|
|
232
|
+
"An invalid store is reported while runtime requests safely inherit. If the configured route or summary fails, DSH preserves the original history. Changes apply from the next compaction request.",
|
|
233
|
+
].join(" "),
|
|
234
|
+
parameters: {
|
|
235
|
+
type: "object",
|
|
236
|
+
additionalProperties: false,
|
|
237
|
+
required: ["action"],
|
|
238
|
+
properties: {
|
|
239
|
+
action: {
|
|
240
|
+
type: "string",
|
|
241
|
+
enum: ["show", "set", "remove"],
|
|
242
|
+
description: "Show the effective target, set a user-specified provider/model and optional reasoning effort, or remove it to restore inheritance.",
|
|
243
|
+
},
|
|
244
|
+
provider: {
|
|
245
|
+
type: "string",
|
|
246
|
+
description: "Provider id explicitly supplied by the user; required for set.",
|
|
247
|
+
},
|
|
248
|
+
model: {
|
|
249
|
+
type: "string",
|
|
250
|
+
description: "Model id explicitly supplied by the user; required for set.",
|
|
251
|
+
},
|
|
252
|
+
reasoningEffort: {
|
|
253
|
+
type: "string",
|
|
254
|
+
description: "Optional compaction reasoning effort explicitly supplied by the user.",
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
output: {
|
|
259
|
+
schema: {
|
|
260
|
+
type: "object",
|
|
261
|
+
additionalProperties: false,
|
|
262
|
+
required: ["action", "configPath", "source", "requiresNextCompaction"],
|
|
263
|
+
properties: {
|
|
264
|
+
action: { type: "string", enum: ["show", "set", "remove"] },
|
|
265
|
+
configPath: { type: "string" },
|
|
266
|
+
source: { type: "string", enum: ["inherit", "persisted"] },
|
|
267
|
+
target: {
|
|
268
|
+
type: "object",
|
|
269
|
+
additionalProperties: false,
|
|
270
|
+
required: ["provider", "model"],
|
|
271
|
+
properties: {
|
|
272
|
+
provider: { type: "string" },
|
|
273
|
+
model: { type: "string" },
|
|
274
|
+
reasoningEffort: { type: "string" },
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
requiresNextCompaction: { type: "boolean" },
|
|
278
|
+
invalidStore: { type: "boolean" },
|
|
279
|
+
recoveredInvalidStore: { type: "boolean" },
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
render(_args, value) {
|
|
283
|
+
const target = value.target
|
|
284
|
+
? `${value.target.provider}/${value.target.model} (reasoning: ${value.target.reasoningEffort ?? "not configured"})`
|
|
285
|
+
: "inherit the current conversation model and reasoning";
|
|
286
|
+
return [{
|
|
287
|
+
type: "text",
|
|
288
|
+
text: [
|
|
289
|
+
`${value.action === "show" ? "Current" : "Updated"} Odai compaction target (${value.source}): ${target}.`,
|
|
290
|
+
" This affects compaction summaries only; other model routes, the summary budget, and cache retention are unchanged.",
|
|
291
|
+
...(value.invalidStore ? [" The persisted store is invalid, so runtime requests currently inherit; set or remove the target to repair it."] : []),
|
|
292
|
+
...(value.recoveredInvalidStore ? [" An invalid prior store was preserved before the configuration was repaired or reset."] : []),
|
|
293
|
+
...(value.requiresNextCompaction ? [" The change applies from the next compaction request."] : []),
|
|
294
|
+
].join(""),
|
|
295
|
+
}];
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
execute(args, execution) {
|
|
299
|
+
if (!execution.agent) throw new Error("odai_compaction_config requires an owning agent session");
|
|
300
|
+
if (isChild(execution.agent)) throw new Error("child agents may not change Odai compaction model configuration");
|
|
301
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) throw new TypeError("arguments must be an object");
|
|
302
|
+
const unknown = Object.keys(args).filter((field) => !["action", "provider", "model", "reasoningEffort"].includes(field));
|
|
303
|
+
if (unknown.length > 0) throw new TypeError(`unknown arguments: ${unknown.join(", ")}`);
|
|
304
|
+
if (!["show", "set", "remove"].includes(args.action)) throw new TypeError("action must be show, set, or remove");
|
|
305
|
+
if (args.action === "show") {
|
|
306
|
+
if (args.provider !== undefined || args.model !== undefined || args.reasoningEffort !== undefined) {
|
|
307
|
+
throw new TypeError("provider, model, and reasoningEffort must be omitted for show");
|
|
308
|
+
}
|
|
309
|
+
let selection;
|
|
310
|
+
try {
|
|
311
|
+
selection = effectiveCompactionTarget(configPath);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
if (!(error instanceof CompactionModelStoreValidationError)) throw error;
|
|
314
|
+
selection = Object.freeze({ source: "inherit", invalidStore: true });
|
|
315
|
+
}
|
|
316
|
+
return Promise.resolve(resultFor(configPath, "show", selection));
|
|
317
|
+
}
|
|
318
|
+
if (args.action === "remove"
|
|
319
|
+
&& (args.provider !== undefined || args.model !== undefined || args.reasoningEffort !== undefined)) {
|
|
320
|
+
throw new TypeError("provider, model, and reasoningEffort must be omitted for remove");
|
|
321
|
+
}
|
|
322
|
+
const proposed = args.action === "set"
|
|
323
|
+
? resolveCompactionTarget({
|
|
324
|
+
provider: args.provider,
|
|
325
|
+
model: args.model,
|
|
326
|
+
...(args.reasoningEffort === undefined ? {} : { reasoningEffort: args.reasoningEffort }),
|
|
327
|
+
})
|
|
328
|
+
: undefined;
|
|
329
|
+
|
|
330
|
+
const releaseLock = acquireOwnedStoreLock(configPath, "Odai compaction model configuration");
|
|
331
|
+
try {
|
|
332
|
+
let recoveredInvalidStore = false;
|
|
333
|
+
try {
|
|
334
|
+
readCompactionModelStore(configPath);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
if (!(error instanceof CompactionModelStoreValidationError)) {
|
|
337
|
+
throw new Error("Odai compaction model configuration could not be read safely; no changes were made", { cause: error });
|
|
338
|
+
}
|
|
339
|
+
recoveredInvalidStore = preserveInvalidStore(configPath);
|
|
340
|
+
}
|
|
341
|
+
if (args.action === "set") writeCompactionModelStore(configPath, proposed);
|
|
342
|
+
else rmSync(configPath, { force: true });
|
|
343
|
+
const selection = args.action === "set"
|
|
344
|
+
? Object.freeze({ target: proposed, source: "persisted" })
|
|
345
|
+
: Object.freeze({ source: "inherit" });
|
|
346
|
+
onConfigured(execution.agent, {
|
|
347
|
+
action: args.action,
|
|
348
|
+
...(selection.target === undefined ? {} : { target: selection.target }),
|
|
349
|
+
...(recoveredInvalidStore ? { recoveredInvalidStore: true } : {}),
|
|
350
|
+
});
|
|
351
|
+
return Promise.resolve(resultFor(configPath, args.action, selection, recoveredInvalidStore));
|
|
352
|
+
} finally {
|
|
353
|
+
releaseLock();
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
}
|
|
@@ -35,6 +35,14 @@ import {
|
|
|
35
35
|
resolveOutputConfigPath,
|
|
36
36
|
} from "./output-config.mjs";
|
|
37
37
|
import { selectSharedOutputPolicyForTurn } from "./output-policy-state.mjs";
|
|
38
|
+
import {
|
|
39
|
+
COMPACTION_CONFIG_PROMPT,
|
|
40
|
+
applyCompactionStateProtocol,
|
|
41
|
+
applyCompactionTarget,
|
|
42
|
+
createCompactionConfigTool,
|
|
43
|
+
effectiveCompactionTarget,
|
|
44
|
+
resolveCompactionConfigPath,
|
|
45
|
+
} from "./compaction-config.mjs";
|
|
38
46
|
import { createSessionEvidence, resolveSessionEvidenceRoot } from "./session-evidence.mjs";
|
|
39
47
|
import {
|
|
40
48
|
SKILL_SOURCE_CONFIG_PROMPT,
|
|
@@ -86,12 +94,13 @@ export function resolveConfig(rawConfig = {}) {
|
|
|
86
94
|
const skillSource = governance.skillSource ?? "bundled";
|
|
87
95
|
const skillConfigPath = resolveSkillSourceConfigPath(governance.skillConfigPath);
|
|
88
96
|
const outputConfigPath = resolveOutputConfigPath(output.configPath);
|
|
97
|
+
const compactionConfigPath = resolveCompactionConfigPath(compaction.configPath);
|
|
89
98
|
const compactionCacheRetention = compaction.cacheRetention
|
|
90
99
|
?? process.env.ODAI_COMPACTION_CACHE_RETENTION
|
|
91
|
-
?? "
|
|
100
|
+
?? "provider-default";
|
|
92
101
|
const unknownRoles = Object.keys(roles).filter((role) => !ROUTED_ROLES.includes(role));
|
|
93
102
|
const unknownOutputFields = Object.keys(output).filter((field) => field !== "configPath");
|
|
94
|
-
const unknownCompactionFields = Object.keys(compaction).filter((field) =>
|
|
103
|
+
const unknownCompactionFields = Object.keys(compaction).filter((field) => !["cacheRetention", "configPath"].includes(field));
|
|
95
104
|
|
|
96
105
|
if (!ROUTING_MODES.has(mode)) {
|
|
97
106
|
throw new TypeError("config.routing.mode must be off, observe, auto, or execute");
|
|
@@ -143,13 +152,16 @@ export function resolveConfig(rawConfig = {}) {
|
|
|
143
152
|
skillConfigPath,
|
|
144
153
|
}),
|
|
145
154
|
output: Object.freeze({ configPath: outputConfigPath }),
|
|
146
|
-
compaction: Object.freeze({
|
|
155
|
+
compaction: Object.freeze({
|
|
156
|
+
cacheRetention: compactionCacheRetention,
|
|
157
|
+
configPath: compactionConfigPath,
|
|
158
|
+
}),
|
|
147
159
|
});
|
|
148
160
|
}
|
|
149
161
|
|
|
150
|
-
|
|
162
|
+
/** Mutate exact-route compaction options; returns true only when an option changed. */
|
|
163
|
+
export function inheritCompactionReasoning(options, sessions, cacheRetention = "provider-default") {
|
|
151
164
|
if (options?.purpose !== "compaction"
|
|
152
|
-
|| options.reasoningEffort !== undefined
|
|
153
165
|
|| options.sessionId === undefined
|
|
154
166
|
|| !Object.isExtensible(options)) {
|
|
155
167
|
return false;
|
|
@@ -163,11 +175,16 @@ export function inheritCompactionReasoning(options, sessions, cacheRetention = "
|
|
|
163
175
|
return false;
|
|
164
176
|
}
|
|
165
177
|
|
|
166
|
-
|
|
178
|
+
let changed = false;
|
|
179
|
+
if (options.reasoningEffort === undefined) {
|
|
180
|
+
options.reasoningEffort = route.reasoningEffort;
|
|
181
|
+
changed = true;
|
|
182
|
+
}
|
|
167
183
|
if (options.cacheRetention === undefined && cacheRetention !== "provider-default") {
|
|
168
184
|
options.cacheRetention = cacheRetention;
|
|
185
|
+
changed = true;
|
|
169
186
|
}
|
|
170
|
-
return
|
|
187
|
+
return changed;
|
|
171
188
|
}
|
|
172
189
|
|
|
173
190
|
export function resolveSkillPath(configuredPath, env = process.env) {
|
|
@@ -382,8 +399,21 @@ export function apply(ctx, rawConfig) {
|
|
|
382
399
|
}
|
|
383
400
|
};
|
|
384
401
|
const hasSessionEvent = (agent, type, predicate) => evidence.has(agent, type, predicate);
|
|
402
|
+
const selectCompactionTarget = () => {
|
|
403
|
+
try {
|
|
404
|
+
return effectiveCompactionTarget(config.compaction.configPath);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
logger.warn(`Odai compaction model configuration is invalid; inheriting the conversation route: ${error instanceof Error ? error.message : String(error)}`);
|
|
407
|
+
return Object.freeze({ source: "inherit", status: "fallback", reasonCode: "compaction-config-invalid" });
|
|
408
|
+
}
|
|
409
|
+
};
|
|
385
410
|
ctx.on("llm/stream", (options, next) => {
|
|
386
|
-
|
|
411
|
+
if (options?.purpose === "compaction") {
|
|
412
|
+
const selection = selectCompactionTarget();
|
|
413
|
+
applyCompactionTarget(options, selection.target, ctx.sessions);
|
|
414
|
+
applyCompactionStateProtocol(options, selection.target);
|
|
415
|
+
inheritCompactionReasoning(options, ctx.sessions, config.compaction.cacheRetention);
|
|
416
|
+
}
|
|
387
417
|
return next();
|
|
388
418
|
});
|
|
389
419
|
const skillPath = resolveSkillPath(config.skillPath);
|
|
@@ -424,6 +454,11 @@ export function apply(ctx, rawConfig) {
|
|
|
424
454
|
order: -17,
|
|
425
455
|
text: "",
|
|
426
456
|
});
|
|
457
|
+
ctx.systemPrompt.section({
|
|
458
|
+
name: "odai:compaction-model-configuration",
|
|
459
|
+
order: -16,
|
|
460
|
+
text: COMPACTION_CONFIG_PROMPT,
|
|
461
|
+
});
|
|
427
462
|
|
|
428
463
|
const optionalSkillRegistry = () => {
|
|
429
464
|
try {
|
|
@@ -568,6 +603,12 @@ export function apply(ctx, rawConfig) {
|
|
|
568
603
|
appendEvent(agent, "odai/output-configured", data);
|
|
569
604
|
},
|
|
570
605
|
}));
|
|
606
|
+
ctx.tools.register(createCompactionConfigTool(config.compaction.configPath, {
|
|
607
|
+
isChild: isSubagent,
|
|
608
|
+
onConfigured(agent, data) {
|
|
609
|
+
appendEvent(agent, "odai/compaction-configured", data);
|
|
610
|
+
},
|
|
611
|
+
}));
|
|
571
612
|
ctx.tools.guard((execution) => childGuard(execution) ?? routeProtectionGuard(execution));
|
|
572
613
|
|
|
573
614
|
ctx.on("tools/result", (execution, result) => {
|