odai-dsh-agent 0.0.5 → 0.0.7

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.
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 on the same provider/model, an omitted summarizer reasoning setting inherits the session's current user-selected reasoning effort so the auxiliary call remains cache-compatible; explicit and cross-model settings are not changed.
17
17
 
18
18
  ## Install
19
19
 
@@ -53,6 +53,22 @@ The controller calls `odai_routing_config` to persist that explicit choice. The
53
53
 
54
54
  Planner, executor, and reviewer are independent optional responsibilities. If any needed one has no mapping, high-impact work fails closed and remains read-only; lower-impact work continues only where it does not depend on the missing independent responsibility. Odai never chooses a model on the user's behalf.
55
55
 
56
+ ## Controller output policy
57
+
58
+ The Agent defaults to **soft concise** output and shares three controller output modes with the Plugin. A user can naturally ask to inspect or change the mode; `odai_output_config` persists an explicit override in `$DSH_HOME/odai/output.json`:
59
+
60
+ | Mode | Policy | Behavior |
61
+ |---|---|---|
62
+ | normal | `concise: false`, no `maxTokens` | use the host's normal presentation and controller budget |
63
+ | soft concise (default) | `concise: true`, no `maxTokens` | shorten only the final user-facing presentation without relaxing required results, evidence, risks, blockers, or verification |
64
+ | economy (optional) | `concise: true`, positive `maxTokens` | add a provider output-ceiling request; default to `500` when the user names economy without another value, or use the user's supplied positive value |
65
+
66
+ Natural requests include `use normal output`, `use soft concise output`, `enable economy mode`, and `set economy mode to 1200 tokens`. Removing the persisted override restores soft concise. Existing pre-mode stores that combined `concise: false` with a ceiling remain readable for compatibility, but new named-mode changes cannot create that legacy combination. The selected mode is stable within one turn and changes from the next user turn.
67
+
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
+
70
+ A same-provider/model compaction that inherits controller reasoning also requests long prompt-cache retention while keeping its independent summary budget. Explicit compaction retention is preserved. `ODAI_COMPACTION_CACHE_RETENTION=provider-default` restores the host default; `short`, `long`, and `none` are also accepted deployment overrides. The first controller request after a landed summary still rebuilds the changed summary prefix.
71
+
56
72
  ## Skill sources
57
73
 
58
74
  The Agent keeps the managed preset's complete skill copy as its `bundled` default, so existing installations do not change behavior. When the user explicitly asks to show, set, or reset the Odai skill source, the controller uses `odai_skill_source_config` and stores the choice in `$DSH_HOME/odai/source.json`, outside the managed preset:
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { readDshVersion } from "../src/dsh-version.mjs";
4
4
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "odai-dsh-agent",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "完整继承 DSH 标准模式 全部能力的 Odai Agent preset。主要用于治理、规划、测试、验证 等,可当成默认总控使用。",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -16,6 +16,7 @@ import {
16
16
  activeRouteProtection,
17
17
  createChildToolGuard,
18
18
  createRouteProtectionGuard,
19
+ isSubagent,
19
20
  summarizeToolResult,
20
21
  } from "./governance.mjs";
21
22
  import {
@@ -26,6 +27,14 @@ import {
26
27
  resolveRoleRoute,
27
28
  resolveRoutingConfigPath,
28
29
  } from "./routing-config.mjs";
30
+ import {
31
+ DEFAULT_OUTPUT_POLICY,
32
+ createOutputConfigTool,
33
+ effectiveOutputPolicy,
34
+ renderOutputPolicyPrompt,
35
+ resolveOutputConfigPath,
36
+ } from "./output-config.mjs";
37
+ import { selectSharedOutputPolicyForTurn } from "./output-policy-state.mjs";
29
38
  import { createSessionEvidence, resolveSessionEvidenceRoot } from "./session-evidence.mjs";
30
39
  import {
31
40
  SKILL_SOURCE_CONFIG_PROMPT,
@@ -46,10 +55,11 @@ import {
46
55
  } from "./skill-selection-state.mjs";
47
56
 
48
57
  export const name = "odai-dsh-runtime";
49
- export const inject = ["systemPrompt", "tools", "subagents"];
58
+ export const inject = ["systemPrompt", "tools", "subagents", "sessions"];
50
59
 
51
60
  const PLUGIN_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
52
61
  const ROUTING_MODES = new Set(["off", "observe", "auto", "execute"]);
62
+ const COMPACTION_CACHE_RETENTIONS = new Set(["provider-default", "short", "long", "none"]);
53
63
  const ROUTED_ROLES = CONFIGURABLE_ROLES;
54
64
  const DEFAULT_ROUTING_MODE = "auto";
55
65
 
@@ -66,6 +76,8 @@ export function resolveConfig(rawConfig = {}) {
66
76
  const routing = assertPlainObject(raw.routing, "config.routing");
67
77
  const roles = assertPlainObject(routing.roles, "config.routing.roles");
68
78
  const governance = assertPlainObject(raw.governance, "config.governance");
79
+ const output = assertPlainObject(raw.output, "config.output");
80
+ const compaction = assertPlainObject(raw.compaction, "config.compaction");
69
81
  const mode = routing.mode ?? DEFAULT_ROUTING_MODE;
70
82
  const provider = routing.provider ?? "spawn";
71
83
  const maxInputChars = routing.maxInputChars ?? 12_000;
@@ -73,7 +85,13 @@ export function resolveConfig(rawConfig = {}) {
73
85
  const additionalDeniedTools = governance.additionalDeniedTools ?? [];
74
86
  const skillSource = governance.skillSource ?? "bundled";
75
87
  const skillConfigPath = resolveSkillSourceConfigPath(governance.skillConfigPath);
88
+ const outputConfigPath = resolveOutputConfigPath(output.configPath);
89
+ const compactionCacheRetention = compaction.cacheRetention
90
+ ?? process.env.ODAI_COMPACTION_CACHE_RETENTION
91
+ ?? "long";
76
92
  const unknownRoles = Object.keys(roles).filter((role) => !ROUTED_ROLES.includes(role));
93
+ const unknownOutputFields = Object.keys(output).filter((field) => field !== "configPath");
94
+ const unknownCompactionFields = Object.keys(compaction).filter((field) => field !== "cacheRetention");
77
95
 
78
96
  if (!ROUTING_MODES.has(mode)) {
79
97
  throw new TypeError("config.routing.mode must be off, observe, auto, or execute");
@@ -87,6 +105,15 @@ export function resolveConfig(rawConfig = {}) {
87
105
  if (unknownRoles.length > 0) {
88
106
  throw new TypeError(`config.routing.roles has unknown roles: ${unknownRoles.join(", ")}`);
89
107
  }
108
+ if (unknownOutputFields.length > 0) {
109
+ throw new TypeError(`config.output has unknown fields: ${unknownOutputFields.join(", ")}`);
110
+ }
111
+ if (unknownCompactionFields.length > 0) {
112
+ throw new TypeError(`config.compaction has unknown fields: ${unknownCompactionFields.join(", ")}`);
113
+ }
114
+ if (!COMPACTION_CACHE_RETENTIONS.has(compactionCacheRetention)) {
115
+ throw new TypeError("config.compaction.cacheRetention must be provider-default, short, long, or none");
116
+ }
90
117
  if (!Array.isArray(additionalDeniedTools)
91
118
  || additionalDeniedTools.some((tool) => typeof tool !== "string" || tool.trim() === "")) {
92
119
  throw new TypeError("config.governance.additionalDeniedTools must be an array of non-empty strings");
@@ -115,9 +142,34 @@ export function resolveConfig(rawConfig = {}) {
115
142
  skillSource,
116
143
  skillConfigPath,
117
144
  }),
145
+ output: Object.freeze({ configPath: outputConfigPath }),
146
+ compaction: Object.freeze({ cacheRetention: compactionCacheRetention }),
118
147
  });
119
148
  }
120
149
 
150
+ export function inheritCompactionReasoning(options, sessions, cacheRetention = "long") {
151
+ if (options?.purpose !== "compaction"
152
+ || options.reasoningEffort !== undefined
153
+ || options.sessionId === undefined
154
+ || !Object.isExtensible(options)) {
155
+ return false;
156
+ }
157
+
158
+ const route = sessions?.get?.(options.sessionId)?.requestHeader?.()?.config;
159
+ if (route?.provider !== options.provider
160
+ || route?.model !== options.model
161
+ || typeof route.reasoningEffort !== "string"
162
+ || route.reasoningEffort.length === 0) {
163
+ return false;
164
+ }
165
+
166
+ options.reasoningEffort = route.reasoningEffort;
167
+ if (options.cacheRetention === undefined && cacheRetention !== "provider-default") {
168
+ options.cacheRetention = cacheRetention;
169
+ }
170
+ return true;
171
+ }
172
+
121
173
  export function resolveSkillPath(configuredPath, env = process.env) {
122
174
  const explicit = configuredPath ?? env.ODAI_SKILL_PATH;
123
175
  if (explicit !== undefined) {
@@ -295,7 +347,7 @@ function canonicalPrompt(selection) {
295
347
  `Canonical skill: ${bundle.manifest.skillVersion}; runtime contract: ${bundle.manifest.runtimeContract}; digest: ${bundle.digest}.`,
296
348
  ...(fallback ? [fallback] : []),
297
349
  "Apply this governance to every request. Keep the controller as the final delivery owner; use another role only for a real independent gap with observable net benefit.",
298
- "Odai governance is already loaded by this runtime; do not call the skill tool to load odai again.",
350
+ "Odai governance is already loaded by this runtime; do not call the skill tool or read SKILL.md to load odai again.",
299
351
  "",
300
352
  bundle.skillText,
301
353
  ].join("\n");
@@ -330,6 +382,10 @@ export function apply(ctx, rawConfig) {
330
382
  }
331
383
  };
332
384
  const hasSessionEvent = (agent, type, predicate) => evidence.has(agent, type, predicate);
385
+ ctx.on("llm/stream", (options, next) => {
386
+ inheritCompactionReasoning(options, ctx.sessions, config.compaction.cacheRetention);
387
+ return next();
388
+ });
333
389
  const skillPath = resolveSkillPath(config.skillPath);
334
390
  const explicitSkillPath = config.skillPath !== undefined
335
391
  || (typeof process.env.ODAI_SKILL_PATH === "string" && process.env.ODAI_SKILL_PATH.trim() !== "");
@@ -363,6 +419,11 @@ export function apply(ctx, rawConfig) {
363
419
  order: -18,
364
420
  text: SKILL_SOURCE_CONFIG_PROMPT,
365
421
  });
422
+ ctx.systemPrompt.section({
423
+ name: "odai:controller-output-policy",
424
+ order: -17,
425
+ text: "",
426
+ });
366
427
 
367
428
  const optionalSkillRegistry = () => {
368
429
  try {
@@ -398,12 +459,28 @@ export function apply(ctx, rawConfig) {
398
459
  skills: optionalSkillRegistry(),
399
460
  });
400
461
  };
462
+ const selectOutputForAgent = () => {
463
+ try {
464
+ return effectiveOutputPolicy(config.output.configPath);
465
+ } catch (error) {
466
+ logger.warn(`Odai output configuration is invalid; using the default policy: ${error instanceof Error ? error.message : String(error)}`);
467
+ return Object.freeze({
468
+ policy: DEFAULT_OUTPUT_POLICY,
469
+ source: "default",
470
+ status: "fallback",
471
+ reasonCode: "output-config-invalid",
472
+ });
473
+ }
474
+ };
401
475
  ctx.on("system-prompt/assemble", async (assembly, context, next) => {
402
476
  const downstream = await next();
403
477
  const agent = context.agent;
404
478
  if (!agent) return downstream;
405
479
  const selection = await selectSharedSkillForTurn(agent, () => selectForAgent(agent, context));
406
480
  const turn = currentAgentTurn(agent);
481
+ const outputSelection = isSubagentSession(agent)
482
+ ? Object.freeze({ policy: DEFAULT_OUTPUT_POLICY, source: "default" })
483
+ : await selectSharedOutputPolicyForTurn(agent, turn, selectOutputForAgent);
407
484
  const selectionEvidence = {
408
485
  ...(turn === undefined ? {} : { turn }),
409
486
  requestedMode: selection.mode,
@@ -421,11 +498,21 @@ export function apply(ctx, rawConfig) {
421
498
  if (selection.status === "fallback") {
422
499
  logger.warn(`Odai skill source ${selection.mode} fell back to bundled governance (${selection.reasonCode})`);
423
500
  }
501
+ const outputPrompt = renderOutputPolicyPrompt(outputSelection.policy);
502
+ if (outputPrompt && !hasSessionEvent(agent, "odai/output-policy-selected", (data) => data?.turn === turn)) {
503
+ appendEvent(agent, "odai/output-policy-selected", {
504
+ ...(turn === undefined ? {} : { turn }),
505
+ source: outputSelection.source,
506
+ policy: outputSelection.policy,
507
+ });
508
+ }
424
509
  return {
425
510
  ...downstream,
426
- sections: downstream.sections.map((section) => section.name === "odai:canonical-governance"
427
- ? { ...section, text: canonicalPrompt(selection) }
428
- : section),
511
+ sections: downstream.sections.map((section) => {
512
+ if (section.name === "odai:canonical-governance") return { ...section, text: canonicalPrompt(selection) };
513
+ if (section.name === "odai:controller-output-policy") return { ...section, text: outputPrompt };
514
+ return section;
515
+ }),
429
516
  };
430
517
  });
431
518
 
@@ -475,6 +562,12 @@ export function apply(ctx, rawConfig) {
475
562
  },
476
563
  },
477
564
  ));
565
+ ctx.tools.register(createOutputConfigTool(config.output.configPath, {
566
+ isChild: isSubagent,
567
+ onConfigured(agent, data) {
568
+ appendEvent(agent, "odai/output-configured", data);
569
+ },
570
+ }));
478
571
  ctx.tools.guard((execution) => childGuard(execution) ?? routeProtectionGuard(execution));
479
572
 
480
573
  ctx.on("tools/result", (execution, result) => {
@@ -484,7 +577,7 @@ export function apply(ctx, rawConfig) {
484
577
  appendEvent(execution.agent, "odai/tool-observed", summary, logger);
485
578
  });
486
579
 
487
- ctx.on("agent/request", async ({ agent, turn }, next) => {
580
+ ctx.on("agent/request", async ({ agent, turn, step }, next) => {
488
581
  const proposed = await next();
489
582
  const childRole = routedRoleOf(agent);
490
583
  const childRoleState = childRole ? configuredRole(childRole) : undefined;
@@ -494,16 +587,37 @@ export function apply(ctx, rawConfig) {
494
587
  : upgrade && upgrade.turn === turn
495
588
  ? upgrade.route
496
589
  : undefined;
497
- if (!roleRoute) return proposed;
498
-
499
- const { reasoningEffort: _inheritedEffort, ...withoutInheritedEffort } = proposed;
500
- return Object.freeze({
501
- ...withoutInheritedEffort,
502
- provider: roleRoute.provider,
503
- model: roleRoute.model,
504
- ...(roleRoute.reasoningEffort === undefined ? {} : { reasoningEffort: roleRoute.reasoningEffort }),
505
- ...(childRole && roleRoute.maxTokens !== undefined ? { maxTokens: roleRoute.maxTokens } : {}),
506
- });
590
+ let request = proposed;
591
+ if (roleRoute) {
592
+ const { reasoningEffort: _inheritedEffort, ...withoutInheritedEffort } = proposed;
593
+ request = Object.freeze({
594
+ ...withoutInheritedEffort,
595
+ provider: roleRoute.provider,
596
+ model: roleRoute.model,
597
+ ...(roleRoute.reasoningEffort === undefined ? {} : { reasoningEffort: roleRoute.reasoningEffort }),
598
+ ...(childRole && roleRoute.maxTokens !== undefined ? { maxTokens: roleRoute.maxTokens } : {}),
599
+ });
600
+ }
601
+ if (childRole || isSubagentSession(agent)) return request;
602
+
603
+ const outputSelection = await selectSharedOutputPolicyForTurn(agent, turn, selectOutputForAgent);
604
+ const configuredMaxTokens = outputSelection.policy.maxTokens;
605
+ if (configuredMaxTokens === undefined) return request;
606
+ const priorMaxTokens = request.maxTokens;
607
+ const effectiveMaxTokens = priorMaxTokens === undefined
608
+ ? configuredMaxTokens
609
+ : Math.min(priorMaxTokens, configuredMaxTokens);
610
+ if (!hasSessionEvent(agent, "odai/output-budget-applied", (data) => data?.turn === turn && data?.step === step)) {
611
+ appendEvent(agent, "odai/output-budget-applied", {
612
+ turn,
613
+ ...(step === undefined ? {} : { step }),
614
+ configuredMaxTokens,
615
+ ...(priorMaxTokens === undefined ? {} : { priorMaxTokens }),
616
+ effectiveMaxTokens,
617
+ semantics: "provider-request-ceiling",
618
+ });
619
+ }
620
+ return Object.freeze({ ...request, maxTokens: effectiveMaxTokens });
507
621
  }, { prepend: true });
508
622
 
509
623
  const protectController = (agent, turn, step, decision, source, failure) => {
@@ -0,0 +1,304 @@
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 POLICY_FIELDS = new Set(["concise", "maxTokens"]);
16
+ const OUTPUT_MODES = new Set(["normal", "concise", "economy"]);
17
+ const DEFAULT_ECONOMY_MAX_TOKENS = 500;
18
+ export const DEFAULT_OUTPUT_POLICY = Object.freeze({ concise: true });
19
+
20
+ export class OutputPolicyStoreValidationError extends Error {}
21
+
22
+ function assertPlainObject(value, field) {
23
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
24
+ throw new TypeError(`${field} must be an object`);
25
+ }
26
+ return value;
27
+ }
28
+
29
+ export function resolveOutputPolicy(value, field = "Odai output policy") {
30
+ const policy = assertPlainObject(value, field);
31
+ const unknown = Object.keys(policy).filter((name) => !POLICY_FIELDS.has(name));
32
+ if (unknown.length > 0) throw new TypeError(`${field} has unknown fields: ${unknown.join(", ")}`);
33
+ if (!Object.hasOwn(policy, "concise") || typeof policy.concise !== "boolean") {
34
+ throw new TypeError(`${field}.concise must be an own boolean property`);
35
+ }
36
+ if (policy.maxTokens !== undefined
37
+ && (!Number.isSafeInteger(policy.maxTokens) || policy.maxTokens <= 0)) {
38
+ throw new TypeError(`${field}.maxTokens must be a positive integer`);
39
+ }
40
+ return Object.freeze({
41
+ concise: policy.concise,
42
+ ...(policy.maxTokens === undefined ? {} : { maxTokens: policy.maxTokens }),
43
+ });
44
+ }
45
+
46
+ export function resolveOutputConfigPath(configuredPath, env = process.env) {
47
+ if (configuredPath !== undefined) {
48
+ if (typeof configuredPath !== "string" || configuredPath.trim() === "") {
49
+ throw new TypeError("config.output.configPath must be a non-empty string");
50
+ }
51
+ return resolve(configuredPath.trim());
52
+ }
53
+ const dshHome = typeof env.DSH_HOME === "string" && env.DSH_HOME.trim() !== ""
54
+ ? resolve(env.DSH_HOME.trim())
55
+ : resolve(homedir(), ".dsh");
56
+ return resolve(dshHome, "odai", "output.json");
57
+ }
58
+
59
+ export function readOutputPolicyStore(configPath) {
60
+ let text;
61
+ try {
62
+ text = readFileSync(configPath, "utf8");
63
+ } catch (error) {
64
+ if (error?.code === "ENOENT") return Object.freeze({ schemaVersion: STORE_SCHEMA_VERSION });
65
+ throw new Error(`cannot read Odai output configuration ${configPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
66
+ }
67
+ let parsed;
68
+ try {
69
+ parsed = JSON.parse(text);
70
+ } catch (error) {
71
+ throw new OutputPolicyStoreValidationError(`Odai output configuration ${configPath} is not valid JSON`, { cause: error });
72
+ }
73
+ try {
74
+ const store = assertPlainObject(parsed, `Odai output configuration ${configPath}`);
75
+ const unknown = Object.keys(store).filter((field) => !["schemaVersion", "policy"].includes(field));
76
+ if (unknown.length > 0) {
77
+ throw new TypeError(`Odai output configuration ${configPath} has unknown fields: ${unknown.join(", ")}`);
78
+ }
79
+ if (store.schemaVersion !== STORE_SCHEMA_VERSION) {
80
+ throw new TypeError(`Odai output configuration ${configPath} has unsupported schemaVersion ${String(store.schemaVersion)}`);
81
+ }
82
+ if (store.policy === undefined) return Object.freeze({ schemaVersion: STORE_SCHEMA_VERSION });
83
+ return Object.freeze({
84
+ schemaVersion: STORE_SCHEMA_VERSION,
85
+ policy: resolveOutputPolicy(store.policy, `Odai output configuration ${configPath}.policy`),
86
+ });
87
+ } catch (error) {
88
+ if (error instanceof OutputPolicyStoreValidationError) throw error;
89
+ throw new OutputPolicyStoreValidationError(
90
+ `Odai output configuration ${configPath} failed validation: ${error instanceof Error ? error.message : String(error)}`,
91
+ { cause: error },
92
+ );
93
+ }
94
+ }
95
+
96
+ export function effectiveOutputPolicy(configPath) {
97
+ const stored = readOutputPolicyStore(configPath);
98
+ return Object.freeze({
99
+ policy: stored.policy ?? DEFAULT_OUTPUT_POLICY,
100
+ source: stored.policy ? "persisted" : "default",
101
+ });
102
+ }
103
+
104
+ function writeOutputPolicyStore(configPath, policy) {
105
+ mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
106
+ const temporary = `${configPath}.tmp-${process.pid}-${randomUUID()}`;
107
+ const value = `${JSON.stringify({ schemaVersion: STORE_SCHEMA_VERSION, policy }, null, 2)}\n`;
108
+ try {
109
+ writeFileSync(temporary, value, { encoding: "utf8", mode: 0o600, flag: "wx" });
110
+ renameSync(temporary, configPath);
111
+ } finally {
112
+ rmSync(temporary, { force: true });
113
+ }
114
+ }
115
+
116
+ function preserveInvalidStore(configPath) {
117
+ try {
118
+ renameSync(configPath, `${configPath}.invalid-${Date.now()}-${randomUUID()}`);
119
+ return true;
120
+ } catch (error) {
121
+ if (error?.code === "ENOENT") return false;
122
+ throw error;
123
+ }
124
+ }
125
+
126
+ function resultFor(configPath, action, selection, recoveredInvalidStore = false) {
127
+ return {
128
+ action,
129
+ configPath,
130
+ policy: selection.policy,
131
+ source: selection.source,
132
+ requiresNextTurn: action !== "show",
133
+ ...(recoveredInvalidStore ? { recoveredInvalidStore: true } : {}),
134
+ };
135
+ }
136
+
137
+ export function renderOutputPolicyPrompt(policy) {
138
+ if (!policy.concise && policy.maxTokens === undefined) return "";
139
+ return [
140
+ "## Odai controller output policy",
141
+ ...(policy.concise ? [
142
+ "Keep the final user-facing response concise. Include only the result, decisive evidence, unresolved items, and necessary next action; omit routine process narration and repeated context unless the user explicitly asks for detail.",
143
+ ] : []),
144
+ ...(policy.maxTokens === undefined ? [] : [
145
+ `Each controller model request carries a provider output ceiling request of ${policy.maxTokens} tokens, which may include reasoning. Provider enforcement is not guaranteed; prioritize completion and finish before the requested ceiling.`,
146
+ ]),
147
+ "This policy applies only to controller requests and the final user-facing response. It never reduces child-agent, compaction, checkpoint, or other internal context budgets.",
148
+ "The policy changes presentation and the requested controller budget only; it never permits omitting required results, evidence, risks, blockers, or verification.",
149
+ ].join("\n");
150
+ }
151
+
152
+ function resolveSetPolicy(args) {
153
+ if (args.mode === undefined) {
154
+ if (args.concise === false && args.maxTokens !== undefined) {
155
+ throw new TypeError("maxTokens requires concise=true or mode=economy");
156
+ }
157
+ return resolveOutputPolicy({
158
+ concise: args.concise,
159
+ ...(args.maxTokens === undefined ? {} : { maxTokens: args.maxTokens }),
160
+ });
161
+ }
162
+ if (!OUTPUT_MODES.has(args.mode)) {
163
+ throw new TypeError("mode must be normal, concise, or economy");
164
+ }
165
+ if (args.concise !== undefined) {
166
+ throw new TypeError("concise must be omitted when mode is supplied");
167
+ }
168
+ if (args.mode !== "economy" && args.maxTokens !== undefined) {
169
+ throw new TypeError("maxTokens is accepted only with economy mode");
170
+ }
171
+ return resolveOutputPolicy({
172
+ concise: args.mode !== "normal",
173
+ ...(args.mode === "economy"
174
+ ? { maxTokens: args.maxTokens ?? DEFAULT_ECONOMY_MAX_TOKENS }
175
+ : {}),
176
+ });
177
+ }
178
+
179
+ export function createOutputConfigTool(configPath, options = {}) {
180
+ const onConfigured = typeof options.onConfigured === "function" ? options.onConfigured : () => {};
181
+ const isChild = typeof options.isChild === "function"
182
+ ? options.isChild
183
+ : (agent) => {
184
+ const header = agent?.session?.header;
185
+ return header?.origin === "subagent"
186
+ || (Number.isSafeInteger(header?.delegationDepth) && header.delegationDepth > 0);
187
+ };
188
+ return {
189
+ name: "odai_output_config",
190
+ description: [
191
+ "Inspect, set, or remove the shared Odai controller output policy; the package default is soft concise.",
192
+ "Normal mode is concise=false with no maxTokens; soft concise is concise=true with no maxTokens; economy mode is concise=true with a user-adjustable maxTokens request ceiling and defaults to 500 only when the user names economy without another value.",
193
+ "Use only when the user explicitly requests an output mode or supplies a custom maxTokens; never invent a non-default custom value. Remove restores soft concise.",
194
+ "Changes start next user turn. maxTokens is a provider request ceiling, not locally enforced; providers may include reasoning, exceed or ignore it, or truncate responses. Child-agent, compaction, checkpoint, and other internal budgets stay unchanged.",
195
+ ].join(" "),
196
+ parameters: {
197
+ type: "object",
198
+ additionalProperties: false,
199
+ required: ["action"],
200
+ properties: {
201
+ action: {
202
+ type: "string",
203
+ enum: ["show", "set", "remove"],
204
+ },
205
+ mode: {
206
+ type: "string",
207
+ enum: ["normal", "concise", "economy"],
208
+ description: "Preferred for set; economy defaults maxTokens to 500 unless the user supplies another positive value.",
209
+ },
210
+ concise: {
211
+ type: "boolean",
212
+ description: "Backward-compatible alternative for set; omit when mode is supplied.",
213
+ },
214
+ maxTokens: {
215
+ type: "integer",
216
+ description: "Optional positive replacement for economy mode's default 500-token provider request ceiling.",
217
+ },
218
+ },
219
+ },
220
+ output: {
221
+ schema: {
222
+ type: "object",
223
+ additionalProperties: false,
224
+ required: ["action", "configPath", "policy", "source", "requiresNextTurn"],
225
+ properties: {
226
+ action: { type: "string", enum: ["show", "set", "remove"] },
227
+ configPath: { type: "string" },
228
+ policy: {
229
+ type: "object",
230
+ additionalProperties: false,
231
+ required: ["concise"],
232
+ properties: {
233
+ concise: { type: "boolean" },
234
+ maxTokens: { type: "integer" },
235
+ },
236
+ },
237
+ source: { type: "string", enum: ["default", "persisted"] },
238
+ requiresNextTurn: { type: "boolean" },
239
+ recoveredInvalidStore: { type: "boolean" },
240
+ },
241
+ },
242
+ render(_args, value) {
243
+ const settings = [
244
+ `concise=${value.policy.concise ? "on" : "off"}`,
245
+ ...(value.policy.maxTokens === undefined ? [] : [`maxTokens=${value.policy.maxTokens}`]),
246
+ ].join(", ");
247
+ return [{
248
+ type: "text",
249
+ text: [
250
+ `${value.action === "show" ? "Current" : "Updated"} Odai controller output policy (${value.source}): ${settings}.`,
251
+ ...(value.policy.maxTokens === undefined ? [] : [" maxTokens is sent as a provider request ceiling; strict provider compliance is not guaranteed and must be checked from usage."]),
252
+ ...(value.recoveredInvalidStore ? [" An invalid prior store was preserved and replaced."] : []),
253
+ ...(value.requiresNextTurn ? [" The change applies from the next user turn."] : []),
254
+ ].join(""),
255
+ }];
256
+ },
257
+ },
258
+ execute(args, execution) {
259
+ if (!execution.agent) throw new Error("odai_output_config requires an owning agent session");
260
+ if (isChild(execution.agent)) throw new Error("child agents may not change Odai output configuration");
261
+ if (!args || typeof args !== "object" || Array.isArray(args)) throw new TypeError("arguments must be an object");
262
+ const unknown = Object.keys(args).filter((field) => !["action", "mode", "concise", "maxTokens"].includes(field));
263
+ if (unknown.length > 0) throw new TypeError(`unknown arguments: ${unknown.join(", ")}`);
264
+ if (!["show", "set", "remove"].includes(args.action)) throw new TypeError("action must be show, set, or remove");
265
+ if (args.action === "show") {
266
+ if (args.mode !== undefined || args.concise !== undefined || args.maxTokens !== undefined) {
267
+ throw new TypeError("mode, concise, and maxTokens must be omitted for show");
268
+ }
269
+ return Promise.resolve(resultFor(configPath, "show", effectiveOutputPolicy(configPath)));
270
+ }
271
+ if (args.action === "remove"
272
+ && (args.mode !== undefined || args.concise !== undefined || args.maxTokens !== undefined)) {
273
+ throw new TypeError("mode, concise, and maxTokens must be omitted for remove");
274
+ }
275
+ const proposed = args.action === "set" ? resolveSetPolicy(args) : undefined;
276
+
277
+ const releaseLock = acquireOwnedStoreLock(configPath, "Odai output configuration");
278
+ try {
279
+ let recoveredInvalidStore = false;
280
+ try {
281
+ readOutputPolicyStore(configPath);
282
+ } catch (error) {
283
+ if (!(error instanceof OutputPolicyStoreValidationError)) {
284
+ throw new Error("Odai output configuration could not be read safely; no changes were made", { cause: error });
285
+ }
286
+ recoveredInvalidStore = preserveInvalidStore(configPath);
287
+ }
288
+ if (args.action === "set") writeOutputPolicyStore(configPath, proposed);
289
+ else rmSync(configPath, { force: true });
290
+ const selection = args.action === "set"
291
+ ? Object.freeze({ policy: proposed, source: "persisted" })
292
+ : Object.freeze({ policy: DEFAULT_OUTPUT_POLICY, source: "default" });
293
+ onConfigured(execution.agent, {
294
+ action: args.action,
295
+ policy: selection.policy,
296
+ ...(recoveredInvalidStore ? { recoveredInvalidStore: true } : {}),
297
+ });
298
+ return Promise.resolve(resultFor(configPath, args.action, selection, recoveredInvalidStore));
299
+ } finally {
300
+ releaseLock();
301
+ }
302
+ },
303
+ };
304
+ }
@@ -0,0 +1,37 @@
1
+ const SHARED_OUTPUT_POLICY_STATE = Symbol.for("odai.dsh.output-policy-state.v1");
2
+
3
+ function sharedStore() {
4
+ if (!globalThis[SHARED_OUTPUT_POLICY_STATE]) {
5
+ Object.defineProperty(globalThis, SHARED_OUTPUT_POLICY_STATE, {
6
+ configurable: false,
7
+ enumerable: false,
8
+ writable: false,
9
+ value: Object.freeze({ selections: new WeakMap() }),
10
+ });
11
+ }
12
+ return globalThis[SHARED_OUTPUT_POLICY_STATE];
13
+ }
14
+
15
+ function generationFor(turn) {
16
+ return Number.isSafeInteger(turn) && turn >= 0 ? turn : "unknown";
17
+ }
18
+
19
+ export async function selectSharedOutputPolicyForTurn(agent, turn, select) {
20
+ if (!agent || typeof agent !== "object") throw new TypeError("an agent is required for Odai output policy selection");
21
+ if (typeof select !== "function") throw new TypeError("an Odai output policy selector is required");
22
+ const generation = generationFor(turn);
23
+ const store = sharedStore();
24
+ const existing = store.selections.get(agent);
25
+ if (existing?.generation === generation) return existing.promise;
26
+
27
+ const state = { generation };
28
+ state.promise = Promise.resolve().then(select).then((selection) => {
29
+ state.selection = selection;
30
+ return selection;
31
+ }).catch((error) => {
32
+ if (store.selections.get(agent) === state) store.selections.delete(agent);
33
+ throw error;
34
+ });
35
+ store.selections.set(agent, state);
36
+ return state.promise;
37
+ }
@@ -0,0 +1,105 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ closeSync,
4
+ fsyncSync,
5
+ mkdirSync,
6
+ openSync,
7
+ readFileSync,
8
+ rmSync,
9
+ writeSync,
10
+ } from "node:fs";
11
+ import { dirname } from "node:path";
12
+
13
+ function readLockOwner(lockPath) {
14
+ try {
15
+ const value = readFileSync(lockPath, "utf8").trim();
16
+ const separator = value.indexOf(":");
17
+ const pid = Number(separator < 0 ? "" : value.slice(0, separator));
18
+ return Number.isSafeInteger(pid) && pid > 0 && separator < value.length - 1
19
+ ? { value, pid }
20
+ : undefined;
21
+ } catch (error) {
22
+ if (error?.code === "ENOENT") return undefined;
23
+ throw error;
24
+ }
25
+ }
26
+
27
+ function processIsAlive(pid) {
28
+ try {
29
+ process.kill(pid, 0);
30
+ return true;
31
+ } catch (error) {
32
+ return error?.code !== "ESRCH";
33
+ }
34
+ }
35
+
36
+ function createLock(lockPath, owner) {
37
+ const handle = openSync(lockPath, "wx", 0o600);
38
+ try {
39
+ const content = Buffer.from(`${owner}\n`, "utf8");
40
+ if (writeSync(handle, content, 0, content.length) !== content.length) {
41
+ throw new Error(`short write while acquiring Odai configuration lock: ${lockPath}`);
42
+ }
43
+ fsyncSync(handle);
44
+ return handle;
45
+ } catch (error) {
46
+ closeSync(handle);
47
+ rmSync(lockPath, { force: true });
48
+ throw error;
49
+ }
50
+ }
51
+
52
+ function releaseLock(handle, lockPath, owner) {
53
+ closeSync(handle);
54
+ const current = readLockOwner(lockPath);
55
+ if (current === undefined) throw new Error(`Odai configuration lock disappeared before release: ${lockPath}`);
56
+ if (current.value !== owner) throw new Error(`Odai configuration lock ownership changed before release: ${lockPath}`);
57
+ rmSync(lockPath);
58
+ }
59
+
60
+ export function acquireOwnedStoreLock(path, label) {
61
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
62
+ const lockPath = `${path}.lock`;
63
+ const claimPath = `${lockPath}.claim`;
64
+ const owner = `${process.pid}:${randomUUID()}`;
65
+ const claimOwner = `${process.pid}:${randomUUID()}`;
66
+ let claimHandle;
67
+ try {
68
+ claimHandle = createLock(claimPath, claimOwner);
69
+ } catch (error) {
70
+ if (error?.code === "EEXIST") {
71
+ throw new Error(`${label} lock acquisition is already in progress; retry the tool call`);
72
+ }
73
+ throw error;
74
+ }
75
+
76
+ let handle;
77
+ try {
78
+ try {
79
+ handle = createLock(lockPath, owner);
80
+ } catch (error) {
81
+ if (error?.code !== "EEXIST") throw error;
82
+ const current = readLockOwner(lockPath);
83
+ if (current === undefined || processIsAlive(current.pid)) {
84
+ throw new Error(`${label} is being updated; retry the tool call`);
85
+ }
86
+ rmSync(lockPath);
87
+ handle = createLock(lockPath, owner);
88
+ }
89
+ } finally {
90
+ try {
91
+ releaseLock(claimHandle, claimPath, claimOwner);
92
+ } catch (error) {
93
+ if (handle !== undefined) {
94
+ try {
95
+ releaseLock(handle, lockPath, owner);
96
+ } catch {}
97
+ handle = undefined;
98
+ }
99
+ throw error;
100
+ }
101
+ }
102
+
103
+ if (handle === undefined) throw new Error(`could not lock ${label}`);
104
+ return () => releaseLock(handle, lockPath, owner);
105
+ }