@piagent/platform 1.6.0 → 1.6.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.
Files changed (213) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +17 -14
  3. package/README.vi.md +26 -1
  4. package/benchmarks/production-v1/spend-control.v1.json +27 -3
  5. package/benchmarks/production-v1/suite.json +5 -1
  6. package/catalog/capabilities.json +16 -16
  7. package/docs/authority-manifest.md +1 -1
  8. package/docs/auto-delegation-policy.md +38 -73
  9. package/docs/command-reference-vietnamese.md +23 -31
  10. package/docs/context-engine.md +5 -3
  11. package/docs/context-window-policy.md +37 -1
  12. package/docs/distribution-standard.md +7 -7
  13. package/docs/en/architecture.md +26 -13
  14. package/docs/languages.json +4 -4
  15. package/docs/operator-manual-vietnamese.md +21 -41
  16. package/docs/publishing-for-teams.md +5 -0
  17. package/docs/quality-benchmark.md +179 -23
  18. package/docs/release-install-policy.md +6 -6
  19. package/docs/runtime-harness-standard.md +3 -3
  20. package/docs/security-threat-model.md +1 -1
  21. package/docs/subagent-orchestration-capabilities.md +19 -51
  22. package/docs/subagents-and-multiagent.md +21 -47
  23. package/docs/task-implementation-contract.md +3 -3
  24. package/docs/task-lifecycle-vietnamese.md +1 -1
  25. package/docs/team-onboarding.md +2 -2
  26. package/docs/vi/architecture.md +26 -13
  27. package/docs-site/architecture.html +1 -1
  28. package/docs-site/benchmark.html +89 -95
  29. package/docs-site/commands.html +1 -1
  30. package/docs-site/content/benchmark.html +88 -94
  31. package/docs-site/content/en/benchmark.html +134 -90
  32. package/docs-site/content/en/index.html +19 -19
  33. package/docs-site/content/en/runtime.html +4 -4
  34. package/docs-site/content/en/whats-new.html +12 -1
  35. package/docs-site/content/en/workflows.html +2 -2
  36. package/docs-site/content/index.html +19 -19
  37. package/docs-site/content/runtime.html +4 -4
  38. package/docs-site/content/whats-new.html +12 -1
  39. package/docs-site/content/workflows.html +2 -2
  40. package/docs-site/context-engine.html +1 -1
  41. package/docs-site/en/architecture.html +1 -1
  42. package/docs-site/en/benchmark.html +135 -91
  43. package/docs-site/en/commands.html +1 -1
  44. package/docs-site/en/context-engine.html +1 -1
  45. package/docs-site/en/index.html +20 -20
  46. package/docs-site/en/mcp-approval.html +1 -1
  47. package/docs-site/en/mcp-auth.html +1 -1
  48. package/docs-site/en/mcp-servers.html +1 -1
  49. package/docs-site/en/mcp.html +1 -1
  50. package/docs-site/en/permissions.html +1 -1
  51. package/docs-site/en/profiles.html +1 -1
  52. package/docs-site/en/quickstart.html +1 -1
  53. package/docs-site/en/runtime.html +5 -5
  54. package/docs-site/en/scope.html +1 -1
  55. package/docs-site/en/security.html +1 -1
  56. package/docs-site/en/team.html +1 -1
  57. package/docs-site/en/whats-new.html +13 -2
  58. package/docs-site/en/workflows.html +3 -3
  59. package/docs-site/index.html +20 -20
  60. package/docs-site/mcp-approval.html +1 -1
  61. package/docs-site/mcp-auth.html +1 -1
  62. package/docs-site/mcp-servers.html +1 -1
  63. package/docs-site/mcp.html +1 -1
  64. package/docs-site/permissions.html +1 -1
  65. package/docs-site/profiles.html +1 -1
  66. package/docs-site/quickstart.html +1 -1
  67. package/docs-site/runtime.html +5 -5
  68. package/docs-site/scope.html +1 -1
  69. package/docs-site/security.html +1 -1
  70. package/docs-site/team.html +1 -1
  71. package/docs-site/whats-new.html +13 -2
  72. package/docs-site/workflows.html +3 -3
  73. package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
  74. package/evals/fixtures/benchmark-assurance-evidence.valid.json +1 -1
  75. package/evals/fixtures/codex-relative-efficiency-v1.invalid.json +4 -0
  76. package/evals/fixtures/codex-relative-efficiency-v1.valid.json +94 -0
  77. package/evals/fixtures/helper-request.invalid.json +1 -1
  78. package/evals/fixtures/helper-request.valid.json +1 -1
  79. package/evals/fixtures/piagent-webui/subagent-tree-v1.valid.json +7 -0
  80. package/evals/fixtures/task-authority-snapshot.valid.json +4 -4
  81. package/evals/fs4-readiness-matrix.v1.json +2 -2
  82. package/evals/fs5-causal-arm.v1.json +2 -2
  83. package/evals/fs5-pilot-protocol.v1.json +3 -3
  84. package/evals/fs5-pilot-protocol.v2.json +8 -8
  85. package/evals/fs5-pilot-protocol.v3.json +8 -8
  86. package/evals/fs5-pilot-protocol.v4.json +6 -6
  87. package/evals/fs5-pilot-protocol.v5.json +7 -6
  88. package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
  89. package/evals/runtime-conformance-v1/README.md +43 -0
  90. package/evals/runtime-conformance-v1/evaluate-paid-canary.mjs +198 -0
  91. package/evals/runtime-conformance-v1/lane.json +57 -0
  92. package/evals/runtime-conformance-v1/paid-canary.json +31 -0
  93. package/evals/runtime-conformance-v1/runner.mjs +456 -0
  94. package/package.json +1 -1
  95. package/packages/piagent-core/README.md +13 -4
  96. package/packages/piagent-core/benchmark/benchmark-codex-relative-efficiency.js +698 -0
  97. package/packages/piagent-core/benchmark/benchmark-core.js +4 -3
  98. package/packages/piagent-core/benchmark/benchmark-forensics.js +4 -2
  99. package/packages/piagent-core/benchmark/benchmark-preflight.js +3 -1
  100. package/packages/piagent-core/benchmark/benchmark-provider-free-evidence.js +345 -0
  101. package/packages/piagent-core/benchmark/benchmark-record-validation.js +74 -5
  102. package/packages/piagent-core/benchmark/benchmark-report.js +15 -1
  103. package/packages/piagent-core/benchmark/benchmark-runtime-causal.js +244 -0
  104. package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +201 -4
  105. package/packages/piagent-core/benchmark/benchmark-suite.js +22 -1
  106. package/packages/piagent-core/benchmark/benchmark-summary-support.js +4 -0
  107. package/packages/piagent-core/benchmark/benchmark-usage.js +15 -4
  108. package/packages/piagent-core/capabilities/runtime-integrity.js +3 -0
  109. package/packages/piagent-core/extensions/acceptance-behavior-proof.js +806 -0
  110. package/packages/piagent-core/extensions/acceptance-receipt.js +89 -48
  111. package/packages/piagent-core/extensions/context-engine.js +1 -11
  112. package/packages/piagent-core/extensions/context-index-policy.js +4 -0
  113. package/packages/piagent-core/extensions/context-query-paths.js +57 -0
  114. package/packages/piagent-core/extensions/criterion-context-pack.js +16 -3
  115. package/packages/piagent-core/extensions/criterion-graph.js +4 -1
  116. package/packages/piagent-core/extensions/fallback-base-policy.ts +101 -0
  117. package/packages/piagent-core/extensions/guard-types.ts +4 -0
  118. package/packages/piagent-core/extensions/piagent-guard.ts +194 -226
  119. package/packages/piagent-core/extensions/task-lifecycle.js +32 -0
  120. package/packages/piagent-core/extensions/task-state.js +69 -8
  121. package/packages/piagent-core/extensions/verification-intelligence.js +49 -12
  122. package/packages/piagent-core/extensions/workspace-evidence-roots.js +52 -16
  123. package/packages/piagent-core/package.json +1 -1
  124. package/packages/piagent-core/policies/base-policy.json +42 -2
  125. package/packages/piagent-core/policy/authority-manifest.v1.json +3 -3
  126. package/packages/piagent-core/prompts/be-to-fe.md +2 -8
  127. package/packages/piagent-core/prompts/plan.md +1 -1
  128. package/packages/piagent-core/prompts/platform-improve.md +2 -6
  129. package/packages/piagent-core/prompts/review.md +2 -7
  130. package/packages/piagent-core/prompts/scout.md +1 -1
  131. package/packages/piagent-core/prompts/task.md +4 -3
  132. package/packages/piagent-core/runtime/hooks/completion-hook.ts +7 -6
  133. package/packages/piagent-core/runtime/hooks/input-hook.ts +6 -0
  134. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +100 -1
  135. package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +58 -72
  136. package/packages/piagent-core/runtime/hooks/tool-result-value-helpers.ts +66 -0
  137. package/packages/piagent-core/runtime/inspection/bounded-emission-guard.ts +286 -0
  138. package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +37 -7
  139. package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +27 -4
  140. package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +20 -3
  141. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +47 -11
  142. package/packages/piagent-core/runtime/orchestration/helper-lifecycle.ts +16 -12
  143. package/packages/piagent-core/runtime/orchestration/minimal-delegation-policy.ts +71 -0
  144. package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +28 -20
  145. package/packages/piagent-core/runtime/orchestration/role-policy.ts +27 -9
  146. package/packages/piagent-core/runtime/orchestration/subagent-tool-policy.ts +329 -0
  147. package/packages/piagent-core/runtime/policy/block-remedy.ts +0 -4
  148. package/packages/piagent-core/runtime/product/efficiency-metrics.ts +3 -0
  149. package/packages/piagent-core/runtime/quality/edit-freshness-guard.ts +102 -0
  150. package/packages/piagent-core/runtime/quality/exact-output-contract.ts +20 -10
  151. package/packages/piagent-core/runtime/quality/performance-assurance.ts +3 -3
  152. package/packages/piagent-core/runtime/recovery/continuation-budget.ts +1 -1
  153. package/packages/piagent-core/runtime/recovery/handoff-projection.ts +4 -1
  154. package/packages/piagent-core/runtime/recovery/pristine-task-policy-refresh.ts +91 -0
  155. package/packages/piagent-core/runtime/recovery/recovery-policy.ts +7 -4
  156. package/packages/piagent-core/runtime/recovery/resume-state.ts +112 -14
  157. package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +7 -13
  158. package/packages/piagent-core/runtime/recovery/task-evidence-coverage-refresh.ts +217 -0
  159. package/packages/piagent-core/runtime/registration/context-commands.ts +16 -2
  160. package/packages/piagent-core/runtime/registration/knowledge-tools.ts +9 -5
  161. package/packages/piagent-core/runtime/registration/operator-request-intake.ts +49 -0
  162. package/packages/piagent-core/runtime/registration/policy-tools.ts +4 -4
  163. package/packages/piagent-core/runtime/registration/session-commands.ts +1 -1
  164. package/packages/piagent-core/runtime/registration/task-completion-tools.ts +12 -9
  165. package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +21 -12
  166. package/packages/piagent-core/runtime/registration/task-start-guidance.ts +19 -13
  167. package/packages/piagent-core/runtime/registration/task-start-manifest.ts +54 -0
  168. package/packages/piagent-core/runtime/registration/task-start-tool.ts +57 -57
  169. package/packages/piagent-core/runtime/runtime-limits.ts +16 -2
  170. package/packages/piagent-core/runtime/session/adaptive-context-analysis.ts +461 -0
  171. package/packages/piagent-core/runtime/session/adaptive-context-governor.ts +464 -0
  172. package/packages/piagent-core/runtime/session/adaptive-context-ledger.ts +265 -0
  173. package/packages/piagent-core/runtime/session/message-signals.ts +7 -3
  174. package/packages/piagent-core/runtime/session/operator-request-carry.ts +44 -0
  175. package/packages/piagent-core/runtime/session/runtime-state.ts +11 -7
  176. package/packages/piagent-core/runtime/session/source-checkout-read-grants.ts +35 -0
  177. package/packages/piagent-core/runtime/session/system-prompt.ts +76 -14
  178. package/packages/piagent-core/runtime/session/task-contract-presentation.ts +39 -0
  179. package/packages/piagent-core/runtime/tools/phase-tools.ts +8 -8
  180. package/packages/piagent-core/runtime/tools/tool-groups.ts +12 -2
  181. package/packages/piagent-core/runtime/verification/exact-verifier-reuse.ts +101 -0
  182. package/packages/piagent-core/runtime/workflows/runtime-intake-compaction.ts +164 -0
  183. package/packages/piagent-core/runtime/workflows/task-intake.ts +167 -39
  184. package/packages/piagent-core/subagents/piagent-oracle.md +5 -3
  185. package/packages/piagent-core/subagents/piagent-planner.md +6 -4
  186. package/packages/piagent-core/subagents/piagent-reviewer.md +5 -3
  187. package/packages/piagent-core/subagents/piagent-scout.md +4 -2
  188. package/packages/piagent-core/subagents/piagent-worker.md +6 -4
  189. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-CxeZqTte.js → MarkdownMessage-B4XehTCU.js} +1 -1
  190. package/packages/piagent-webui/dist/client/assets/{index-DynIMe8I.js → index-CgOEyRPG.js} +4 -4
  191. package/packages/piagent-webui/dist/client/index.html +1 -1
  192. package/packages/piagent-webui/extension/piagent-webui.ts +3 -1
  193. package/packages/piagent-webui/extension/same-session-bridge.ts +1 -1
  194. package/packages/piagent-webui/extension/session-stream-adapter.ts +33 -10
  195. package/packages/piagent-webui/gateway/gateway-session-stream.ts +37 -21
  196. package/packages/piagent-webui/gateway/session-operation-lifecycle.ts +237 -0
  197. package/packages/piagent-webui/gateway/session-operation-retry-control.ts +38 -0
  198. package/packages/piagent-webui/gateway/session-operation-watchdog.ts +9 -2
  199. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +8 -7
  200. package/packages/piagent-webui/ownership/session-lease-store.ts +5 -2
  201. package/packages/piagent-webui/server/core-inspection-provider.ts +2 -1
  202. package/packages/piagent-webui/server/transcript-projection.ts +91 -5
  203. package/schemas/codex-relative-efficiency-v1.schema.json +155 -0
  204. package/schemas/piagent-webui/runtime-event-v2.schema.json +15 -5
  205. package/schemas/piagent-webui/subagent-tree-v1.schema.json +11 -1
  206. package/schemas/project-profile.schema.json +1 -1
  207. package/schemas/task-contract.schema.json +15 -0
  208. package/scripts/benchmark-runner-core.mjs +14 -6
  209. package/scripts/benchmark-runner-finalization.mjs +4 -0
  210. package/scripts/configure-subagents.sh +42 -34
  211. package/scripts/helper-evaluation.mjs +1 -1
  212. package/scripts/team-doctor.sh +1 -1
  213. package/scripts/verify-local.sh +5 -0
@@ -0,0 +1,461 @@
1
+ import crypto from "node:crypto";
2
+
3
+ import { redactSensitiveText } from "../../extensions/redaction-core.js";
4
+ import {
5
+ CONTEXT_GOVERNOR_PRESSURE_TOKENS,
6
+ CONTEXT_GOVERNOR_SEMANTIC_BOUNDARY_TOKENS,
7
+ CONTEXT_GOVERNOR_TARGET_MESSAGE_TOKENS,
8
+ CONTEXT_GOVERNOR_TOOL_ROUND_LIMIT,
9
+ CONTEXT_PROVIDER_PROMPT_CEILING_TOKENS
10
+ } from "../runtime-limits.ts";
11
+
12
+ export const GOVERNOR_VERSION = "adaptive-context-governor-v2" as const;
13
+ export const CONTEXT_GOVERNOR_MIN_SAVINGS_FLOOR_TOKENS = 8_000;
14
+ export const CONTEXT_GOVERNOR_MIN_SAVINGS_CEILING_TOKENS = 20_000;
15
+ export const CONTEXT_GOVERNOR_MIN_SAVINGS_RATIO = 0.15;
16
+ export const CONTEXT_GOVERNOR_PROTECTED_BOUNDARY_GROUPS = 6;
17
+ const READ_TOOLS = new Set([
18
+ "read",
19
+ "grep",
20
+ "find",
21
+ "ls",
22
+ "glob",
23
+ "search",
24
+ "fetch_content",
25
+ "get_search_content",
26
+ "web_search"
27
+ ]);
28
+ const MUTATION_TOOLS = new Set(["edit", "write", "apply_patch", "patch"]);
29
+ const VERIFIER_PATTERN = /(?:^|\s)(?:test|tests|lint|typecheck|check|verify|build|pytest|vitest|jest|playwright|cargo\s+test|go\s+test)(?:\s|:|$)/i;
30
+ const MUTATION_COMMAND_PATTERN = /(?:^|\s)(?:sed\s+-i|perl\s+-pi|tee\s+|cp\s+|mv\s+|mkdir\s+|touch\s+|npm\s+version|git\s+apply)(?:\s|$)|(?:>>?|2>)\s*[^&]/i;
31
+
32
+ export type MessageLike = Record<string, any> & { role?: string; timestamp?: number };
33
+
34
+ export type SemanticPhase = "scout" | "plan" | "implement" | "verify" | "review" | "release" | "discuss" | "unknown";
35
+ export type SemanticBoundary = {
36
+ kind: "none" | "phase" | "task";
37
+ previousPhase: SemanticPhase;
38
+ currentPhase: SemanticPhase;
39
+ similarity: number;
40
+ };
41
+
42
+ export type ContextResidencyMetrics = {
43
+ estimatedMessageTokens: number;
44
+ toolResults: number;
45
+ readResults: number;
46
+ mutationResults: number;
47
+ verifierResults: number;
48
+ failedResults: number;
49
+ duplicateResults: number;
50
+ duplicateResultRate: number;
51
+ lowValueTokenShare: number;
52
+ userTurns: number;
53
+ };
54
+
55
+ export type ContextGovernorDecision = {
56
+ version: typeof GOVERNOR_VERSION;
57
+ action: "passthrough" | "project";
58
+ reason: "none" | "semantic-boundary" | "residency" | "pressure" | "provider-ceiling";
59
+ reasonCodes: string[];
60
+ reportedTokens: number;
61
+ effectiveTokens: number;
62
+ contextWindow: number;
63
+ percent: number;
64
+ originalMessageTokens: number;
65
+ projectedMessageTokens: number;
66
+ candidateProjectedMessageTokens: number;
67
+ estimatedSavingsTokens: number;
68
+ minimumSavingsTokens: number;
69
+ savingsRatio: number;
70
+ fallback: "none" | "no-safe-boundary" | "insufficient-savings" | "unsafe-tool-protocol";
71
+ originalMessages: number;
72
+ projectedMessages: number;
73
+ accounting: ContextGovernorAccounting;
74
+ boundary: SemanticBoundary;
75
+ residency: ContextResidencyMetrics;
76
+ };
77
+
78
+ export type ContextGovernorAccounting = {
79
+ contextOccupancy: {
80
+ providerReportedTokens: number | null;
81
+ transcriptEstimatedTokens: number;
82
+ projectedTranscriptTokens: number;
83
+ effectivePressureTokens: number;
84
+ contextWindow: number;
85
+ };
86
+ billedTraffic: {
87
+ measured: false;
88
+ inputTokens: null;
89
+ outputTokens: null;
90
+ cacheReadTokens: null;
91
+ governorProviderCalls: 0;
92
+ reason: "provider-billing-not-exposed-by-context-hook";
93
+ };
94
+ };
95
+
96
+ export type ToolProtocolAudit = {
97
+ intact: boolean;
98
+ orphanCallIds: string[];
99
+ orphanResultIds: string[];
100
+ };
101
+
102
+ export type ToolObservation = {
103
+ identity: string;
104
+ name: string;
105
+ target: string;
106
+ kind: "read" | "mutation" | "verify" | "other";
107
+ isError: boolean;
108
+ tokens: number;
109
+ };
110
+
111
+ function bounded(value: unknown, maxChars: number): string {
112
+ const compact = String(value ?? "").replace(/\s+/g, " ").trim();
113
+ if (compact.length <= maxChars) return compact;
114
+ const marker = " ... ";
115
+ const available = Math.max(0, maxChars - marker.length);
116
+ const head = Math.floor(available * 0.68);
117
+ return `${compact.slice(0, head).trimEnd()}${marker}${compact.slice(-(available - head)).trimStart()}`;
118
+ }
119
+
120
+ export function safeText(value: unknown, maxChars: number): string {
121
+ return bounded(redactSensitiveText(String(value ?? "")).text, maxChars);
122
+ }
123
+
124
+ function contentTokenEstimate(content: unknown): number {
125
+ if (typeof content === "string") return Math.ceil(content.length / 4);
126
+ if (!Array.isArray(content)) return 0;
127
+ let tokens = 0;
128
+ for (const block of content) {
129
+ if (!block || typeof block !== "object") continue;
130
+ if (block.type === "image") {
131
+ tokens += 1_200;
132
+ continue;
133
+ }
134
+ if (block.type === "text") tokens += Math.ceil(String(block.text ?? "").length / 4);
135
+ else if (block.type === "thinking") tokens += Math.ceil(String(block.thinking ?? block.thinkingSignature ?? "").length / 4);
136
+ else if (block.type === "toolCall") {
137
+ let serialized = "";
138
+ try {
139
+ serialized = JSON.stringify(block.arguments ?? {});
140
+ } catch {
141
+ serialized = String(block.arguments ?? "");
142
+ }
143
+ tokens += Math.ceil((String(block.name ?? "").length + serialized.length) / 4) + 8;
144
+ }
145
+ }
146
+ return tokens;
147
+ }
148
+
149
+ export function estimateGovernorMessageTokens(message: MessageLike): number {
150
+ if (!message || typeof message !== "object") return 0;
151
+ if (message.role === "compactionSummary" || message.role === "branchSummary") {
152
+ return Math.ceil(String(message.summary ?? "").length / 4) + 16;
153
+ }
154
+ if (message.role === "bashExecution") {
155
+ return message.excludeFromContext ? 0 : Math.ceil((String(message.command ?? "").length + String(message.output ?? "").length) / 4) + 16;
156
+ }
157
+ return contentTokenEstimate(message.content) + 8;
158
+ }
159
+
160
+ export function estimateGovernorMessagesTokens(messages: MessageLike[]): number {
161
+ return messages.reduce((sum, message) => sum + estimateGovernorMessageTokens(message), 0);
162
+ }
163
+
164
+ export function messageText(message: MessageLike): string {
165
+ if (typeof message.content === "string") return message.content;
166
+ if (!Array.isArray(message.content)) return "";
167
+ return message.content
168
+ .filter((block: any) => block?.type === "text")
169
+ .map((block: any) => String(block.text ?? ""))
170
+ .join("\n");
171
+ }
172
+
173
+ function fold(value: string): string {
174
+ return value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
175
+ }
176
+
177
+ export function classifySemanticPhase(value: string): SemanticPhase {
178
+ const text = fold(value);
179
+ if (/\b(commit|push|merge|publish|release|tag|deploy|phat hanh|trien khai)\b/.test(text)) return "release";
180
+ if (/\b(verify|verification|run tests?|test lai|kiem thu|typecheck|lint|ci|closeout)\b/.test(text)) return "verify";
181
+ if (/\b(implement|implementation|fix|repair|code|sua code|tien hanh sua|lam di|build it)\b/.test(text)) return "implement";
182
+ if (/\b(plan|planning|ke hoach|len plan|proposal|design approach)\b/.test(text)) return "plan";
183
+ if (/\b(review|reviewer|cross-check|double-check|danh gia lai)\b/.test(text)) return "review";
184
+ if (/\b(scout|audit|inspect|investigate|read-only|read only|mapping|map logic|kiem tra logic|xem xet)\b/.test(text)) return "scout";
185
+ if (/\b(explain|why|discuss|question|tai sao|vi sao|nghi sao|phan tich)\b/.test(text)) return "discuss";
186
+ return "unknown";
187
+ }
188
+
189
+ const SEMANTIC_STOP_WORDS = new Set([
190
+ "about", "after", "agent", "anh", "before", "cho", "code", "continue", "cua", "em", "file", "fix", "giup",
191
+ "implement", "kiem", "lam", "logic", "please", "project", "sua", "task", "test", "tiep", "tuc", "that", "the",
192
+ "this", "them", "then", "there", "these", "update", "verify", "viec", "with", "xem"
193
+ ]);
194
+
195
+ function semanticTerms(value: string): Set<string> {
196
+ const terms = fold(value).match(/[a-z0-9_.\/-]{3,}/g) ?? [];
197
+ return new Set(terms.filter((term) => !SEMANTIC_STOP_WORDS.has(term)));
198
+ }
199
+
200
+ function semanticSimilarity(left: string, right: string): number {
201
+ const a = semanticTerms(left);
202
+ const b = semanticTerms(right);
203
+ if (a.size === 0 || b.size === 0) return 0;
204
+ let intersection = 0;
205
+ for (const term of a) if (b.has(term)) intersection += 1;
206
+ return intersection / new Set([...a, ...b]).size;
207
+ }
208
+
209
+ export function operatorMessages(messages: MessageLike[]): MessageLike[] {
210
+ return messages.filter((message) => message.role === "user" && messageText(message).trim());
211
+ }
212
+
213
+ export function detectSemanticBoundary(messages: MessageLike[]): SemanticBoundary {
214
+ const users = operatorMessages(messages);
215
+ const currentText = messageText(users.at(-1) ?? {});
216
+ const previousText = messageText(users.at(-2) ?? {});
217
+ const currentPhase = classifySemanticPhase(currentText);
218
+ const previousPhase = classifySemanticPhase(previousText);
219
+ const similarity = semanticSimilarity(previousText, currentText);
220
+ if (!previousText || !currentText) return { kind: "none", previousPhase, currentPhase, similarity };
221
+
222
+ const phaseChanged = currentPhase !== "unknown"
223
+ && previousPhase !== "unknown"
224
+ && currentPhase !== previousPhase
225
+ && ["plan", "implement", "verify", "review", "release"].includes(currentPhase);
226
+ if (phaseChanged) return { kind: "phase", previousPhase, currentPhase, similarity };
227
+
228
+ const continuation = /\b(continue|tiep tuc|proceed|carry on|oke|ok|do it|lam tiep|sua tiep)\b/.test(fold(currentText));
229
+ if (!continuation && currentText.length >= 24 && previousText.length >= 24 && similarity < 0.12) {
230
+ return { kind: "task", previousPhase, currentPhase, similarity };
231
+ }
232
+ return { kind: "none", previousPhase, currentPhase, similarity };
233
+ }
234
+
235
+ function toolCalls(message: MessageLike): any[] {
236
+ if (message.role !== "assistant" || !Array.isArray(message.content)) return [];
237
+ return message.content.filter((block: any) => block?.type === "toolCall");
238
+ }
239
+
240
+ function toolCallId(block: any): string {
241
+ return String(block?.id ?? block?.toolCallId ?? "").trim();
242
+ }
243
+
244
+ export function auditToolProtocol(messages: MessageLike[]): ToolProtocolAudit {
245
+ const calls = new Map<string, number>();
246
+ const results = new Map<string, number>();
247
+ for (const message of messages) {
248
+ for (const call of toolCalls(message)) {
249
+ const id = toolCallId(call);
250
+ if (id) calls.set(id, (calls.get(id) ?? 0) + 1);
251
+ }
252
+ if (message.role !== "toolResult") continue;
253
+ const id = String(message.toolCallId ?? "").trim();
254
+ if (id) results.set(id, (results.get(id) ?? 0) + 1);
255
+ }
256
+ const orphanCallIds = [...calls]
257
+ .filter(([id, count]) => (results.get(id) ?? 0) !== count)
258
+ .map(([id]) => id)
259
+ .sort();
260
+ const orphanResultIds = [...results]
261
+ .filter(([id, count]) => (calls.get(id) ?? 0) !== count)
262
+ .map(([id]) => id)
263
+ .sort();
264
+ return {
265
+ intact: orphanCallIds.length === 0 && orphanResultIds.length === 0,
266
+ orphanCallIds,
267
+ orphanResultIds
268
+ };
269
+ }
270
+
271
+ function canonicalToolArguments(value: unknown): string {
272
+ const canonical = (item: unknown): unknown => {
273
+ if (Array.isArray(item)) return item.map(canonical);
274
+ if (!item || typeof item !== "object") return item;
275
+ return Object.fromEntries(
276
+ Object.entries(item as Record<string, unknown>)
277
+ .filter(([, child]) => child !== undefined)
278
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
279
+ .map(([key, child]) => [key, canonical(child)])
280
+ );
281
+ };
282
+ try {
283
+ return JSON.stringify(canonical(value));
284
+ } catch {
285
+ return String(value ?? "");
286
+ }
287
+ }
288
+
289
+ function toolTarget(name: string, input: Record<string, unknown>): string {
290
+ const candidate = input.url ?? input.path ?? input.file_path ?? input.filePath ?? input.target ?? input.query ?? input.pattern
291
+ ?? input.command ?? input.cmd ?? input.script ?? "";
292
+ if (candidate) return safeText(candidate, name === "bash" ? 320 : 180);
293
+ if (Array.isArray(input.queries) && input.queries.length > 0) {
294
+ return safeText(input.queries.join(" | "), 180);
295
+ }
296
+ const selector = [
297
+ input.responseId ?? input.response_id,
298
+ input.urlIndex ?? input.url_index,
299
+ input.offset,
300
+ input.limit
301
+ ];
302
+ if (selector.some((value) => value !== undefined && value !== null && value !== "")) {
303
+ return safeText([
304
+ selector[0] === undefined ? "" : `responseId=${String(selector[0])}`,
305
+ selector[1] === undefined ? "" : `urlIndex=${String(selector[1])}`,
306
+ selector[2] === undefined ? "" : `offset=${String(selector[2])}`,
307
+ selector[3] === undefined ? "" : `limit=${String(selector[3])}`
308
+ ].filter(Boolean).join(" "), 180);
309
+ }
310
+ return safeText(candidate, name === "bash" ? 320 : 180);
311
+ }
312
+
313
+ function toolKind(name: string, target: string): ToolObservation["kind"] {
314
+ const normalized = name.toLowerCase();
315
+ if (READ_TOOLS.has(normalized)) return "read";
316
+ if ((normalized === "bash" || normalized === "exec" || normalized === "exec_command") && VERIFIER_PATTERN.test(target)) return "verify";
317
+ if (MUTATION_TOOLS.has(normalized) || (normalized === "bash" && MUTATION_COMMAND_PATTERN.test(target))) return "mutation";
318
+ return "other";
319
+ }
320
+
321
+ export function toolObservations(messages: MessageLike[]): ToolObservation[] {
322
+ const calls = new Map<string, { name: string; input: Record<string, unknown> }>();
323
+ const observations: ToolObservation[] = [];
324
+ for (const message of messages) {
325
+ for (const call of toolCalls(message)) {
326
+ calls.set(String(call.id ?? ""), {
327
+ name: String(call.name ?? "tool"),
328
+ input: call.arguments && typeof call.arguments === "object" ? call.arguments : {}
329
+ });
330
+ }
331
+ if (message.role !== "toolResult") continue;
332
+ const call = calls.get(String(message.toolCallId ?? ""));
333
+ const name = String(message.toolName ?? call?.name ?? "tool").toLowerCase();
334
+ const input = call?.input ?? {};
335
+ const target = toolTarget(name, input);
336
+ const identitySource = `${name}\u0000${canonicalToolArguments(input)}`;
337
+ observations.push({
338
+ identity: crypto.createHash("sha256").update(identitySource).digest("hex").slice(0, 16),
339
+ name,
340
+ target,
341
+ kind: toolKind(name, target),
342
+ isError: message.isError === true,
343
+ tokens: estimateGovernorMessageTokens(message)
344
+ });
345
+ }
346
+ return observations;
347
+ }
348
+
349
+ export function analyzeContextResidency(messages: MessageLike[]): ContextResidencyMetrics {
350
+ const observations = toolObservations(messages);
351
+ const seen = new Set<string>();
352
+ let duplicateResults = 0;
353
+ let lowValueTokens = 0;
354
+ for (const observation of observations) {
355
+ if (seen.has(observation.identity)) duplicateResults += 1;
356
+ seen.add(observation.identity);
357
+ if (observation.kind === "read" && !observation.isError) lowValueTokens += observation.tokens;
358
+ }
359
+ const estimatedMessageTokens = estimateGovernorMessagesTokens(messages);
360
+ return {
361
+ estimatedMessageTokens,
362
+ toolResults: observations.length,
363
+ readResults: observations.filter((item) => item.kind === "read").length,
364
+ mutationResults: observations.filter((item) => item.kind === "mutation").length,
365
+ verifierResults: observations.filter((item) => item.kind === "verify").length,
366
+ failedResults: observations.filter((item) => item.isError).length,
367
+ duplicateResults,
368
+ duplicateResultRate: observations.length > 0 ? duplicateResults / observations.length : 0,
369
+ lowValueTokenShare: estimatedMessageTokens > 0 ? lowValueTokens / estimatedMessageTokens : 0,
370
+ userTurns: operatorMessages(messages).length
371
+ };
372
+ }
373
+
374
+ export function reasonForProjection(
375
+ effectiveTokens: number,
376
+ contextWindow: number,
377
+ boundary: SemanticBoundary,
378
+ residency: ContextResidencyMetrics
379
+ ): { reason: ContextGovernorDecision["reason"]; codes: string[] } {
380
+ const percent = contextWindow > 0 ? (effectiveTokens / contextWindow) * 100 : 0;
381
+ if (effectiveTokens >= CONTEXT_PROVIDER_PROMPT_CEILING_TOKENS || percent >= 68) {
382
+ return { reason: "provider-ceiling", codes: ["provider-prompt-headroom", "preserve-output-budget"] };
383
+ }
384
+ if (boundary.kind !== "none" && (
385
+ effectiveTokens >= CONTEXT_GOVERNOR_SEMANTIC_BOUNDARY_TOKENS
386
+ || residency.toolResults >= 8
387
+ )) {
388
+ return { reason: "semantic-boundary", codes: [`${boundary.kind}-boundary`, "fresh-working-set"] };
389
+ }
390
+ if (residency.toolResults >= CONTEXT_GOVERNOR_TOOL_ROUND_LIMIT && (
391
+ residency.duplicateResultRate >= 0.12
392
+ || residency.readResults >= Math.ceil(CONTEXT_GOVERNOR_TOOL_ROUND_LIMIT * 0.5)
393
+ || effectiveTokens >= CONTEXT_GOVERNOR_TARGET_MESSAGE_TOKENS
394
+ )) {
395
+ return { reason: "residency", codes: ["tool-round-residency", residency.duplicateResults > 0 ? "duplicate-delta" : "stale-read-residency"] };
396
+ }
397
+ if ((effectiveTokens >= CONTEXT_GOVERNOR_PRESSURE_TOKENS || percent >= 52)
398
+ && residency.toolResults >= 8) {
399
+ return { reason: "pressure", codes: ["context-pressure", "value-density"] };
400
+ }
401
+ return { reason: "none", codes: [] };
402
+ }
403
+
404
+ export function minimumContextSavingsTokens(occupancyTokens: number): number {
405
+ const proportional = Math.ceil(Math.max(0, occupancyTokens) * CONTEXT_GOVERNOR_MIN_SAVINGS_RATIO);
406
+ return Math.max(
407
+ CONTEXT_GOVERNOR_MIN_SAVINGS_FLOOR_TOKENS,
408
+ Math.min(CONTEXT_GOVERNOR_MIN_SAVINGS_CEILING_TOKENS, proportional)
409
+ );
410
+ }
411
+
412
+ function toolProtocolSafeCuts(messages: MessageLike[]): number[] {
413
+ const callIndexes = new Map<string, number>();
414
+ const pairSpans: Array<{ call: number; result: number }> = [];
415
+ for (let index = 0; index < messages.length; index += 1) {
416
+ const message = messages[index]!;
417
+ for (const call of toolCalls(message)) {
418
+ const id = toolCallId(call);
419
+ if (id && !callIndexes.has(id)) callIndexes.set(id, index);
420
+ }
421
+ if (message.role !== "toolResult") continue;
422
+ const id = String(message.toolCallId ?? "").trim();
423
+ const call = callIndexes.get(id);
424
+ if (call !== undefined) pairSpans.push({ call, result: index });
425
+ }
426
+
427
+ const blockedDeltas = new Int32Array(messages.length + 2);
428
+ for (const span of pairSpans) {
429
+ blockedDeltas[span.call + 1] += 1;
430
+ blockedDeltas[span.result + 1] -= 1;
431
+ }
432
+ const cuts: number[] = [];
433
+ let blocked = 0;
434
+ for (let cut = 0; cut <= messages.length; cut += 1) {
435
+ blocked += blockedDeltas[cut] ?? 0;
436
+ if (blocked > 0) continue;
437
+ cuts.push(cut);
438
+ }
439
+ return cuts;
440
+ }
441
+
442
+ export function safeSuffixStart(
443
+ messages: MessageLike[],
444
+ recentTokenBudget: number,
445
+ protectedBoundaryGroups = CONTEXT_GOVERNOR_PROTECTED_BOUNDARY_GROUPS
446
+ ): number {
447
+ if (!auditToolProtocol(messages).intact) return 0;
448
+ let tokens = 0;
449
+ let rawStart = messages.length;
450
+ while (rawStart > 0 && tokens < recentTokenBudget) {
451
+ rawStart -= 1;
452
+ tokens += estimateGovernorMessageTokens(messages[rawStart]!);
453
+ }
454
+
455
+ const safeCuts = toolProtocolSafeCuts(messages);
456
+ const budgetStart = [...safeCuts].reverse().find((cut) => cut <= rawStart) ?? 0;
457
+ const protectedGroups = Math.max(0, Math.floor(protectedBoundaryGroups));
458
+ const protectedIndex = Math.max(0, safeCuts.length - 1 - protectedGroups);
459
+ const protectedStart = safeCuts[protectedIndex] ?? 0;
460
+ return Math.min(budgetStart, protectedStart);
461
+ }