opencode-wait-a-minute 0.0.0-stage → 1.1.0-rc.10
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/LICENSE +21 -0
- package/README.md +318 -2
- package/index.js +1836 -0
- package/package.json +70 -4
- package/skills/codebase-design/SKILL.md +114 -0
- package/skills/design-contract/SKILL.md +27 -0
- package/skills/design-critique/SKILL.md +19 -0
- package/skills/design-system/SKILL.md +17 -0
- package/skills/design-system/references/image-analysis.md +14 -0
- package/skills/design-system/references/tokens.md +32 -0
- package/skills/design-system/references/ui-rules.md +14 -0
- package/skills/diagnosing-bugs/SKILL.md +138 -0
- package/skills/frontend-design/SKILL.md +36 -0
- package/skills/handoff/SKILL.md +16 -0
- package/skills/improve-codebase-architecture/SKILL.md +71 -0
- package/skills/linter-negative/SKILL.md +74 -0
- package/skills/no-slop-ui/SKILL.md +26 -0
- package/skills/prototype/SKILL.md +26 -0
- package/skills/registry.json +44087 -0
- package/skills/tastemaker/SKILL.md +8 -0
- package/skills/tastemaker/references/tastemaker.md +14 -0
- package/skills/tdd/SKILL.md +38 -0
- package/skills/visual-review/SKILL.md +103 -0
- package/skills/visual-review/references/review-gates.md +42 -0
- package/skills/visual-review/references/surface-archetypes.md +37 -0
- package/skills/wizard/SKILL.md +44 -0
- package/skills/writing-for-agents/SKILL.md +81 -0
- package/src/cognition/cognition-store.js +251 -0
- package/src/cognition/cognitive-state.js +126 -0
- package/src/cognition/hypothesis-manager.js +113 -0
- package/src/cognition/observation-engine.js +112 -0
- package/src/cognition/observation-provenance.js +268 -0
- package/src/context/active-context-boundary.js +68 -0
- package/src/context/assembly.js +494 -0
- package/src/context/assessment-rules.js +55 -0
- package/src/context/capsule-staleness.js +267 -0
- package/src/context/capsules.js +2 -0
- package/src/context/compare-array.js +98 -0
- package/src/context/compare-object.js +89 -0
- package/src/context/compare-values.js +42 -0
- package/src/context/context-assembly-contract.js +71 -0
- package/src/context/context-budget-manager.js +232 -0
- package/src/context/context-builder.js +49 -0
- package/src/context/context-cache.js +55 -0
- package/src/context/context-capture.js +243 -0
- package/src/context/context-classification.js +199 -0
- package/src/context/context-compaction.js +179 -0
- package/src/context/context-decision-audit.js +267 -0
- package/src/context/context-demotion.js +173 -0
- package/src/context/context-dictionary.js +106 -0
- package/src/context/context-drift-detection.js +225 -0
- package/src/context/context-evaluation.js +374 -0
- package/src/context/context-event-ingress.js +100 -0
- package/src/context/context-event-normalization.js +224 -0
- package/src/context/context-feedback-loop.js +216 -0
- package/src/context/context-freshness.js +21 -0
- package/src/context/context-graph-builder.js +71 -0
- package/src/context/context-graph.js +451 -0
- package/src/context/context-history-relevance.js +321 -0
- package/src/context/context-interception.js +145 -0
- package/src/context/context-invalidation-events.js +154 -0
- package/src/context/context-lifecycle.js +170 -0
- package/src/context/context-loop-prevention.js +347 -0
- package/src/context/context-manager.js +340 -0
- package/src/context/context-memory-layers.js +265 -0
- package/src/context/context-minimality.js +166 -0
- package/src/context/context-normalization.js +130 -0
- package/src/context/context-optimization-metrics.js +434 -0
- package/src/context/context-output-policy.js +135 -0
- package/src/context/context-pack-authority.js +157 -0
- package/src/context/context-promotion.js +172 -0
- package/src/context/context-query-contract.js +101 -0
- package/src/context/context-ranking.js +164 -0
- package/src/context/context-rate-distortion.js +261 -0
- package/src/context/context-recovery.js +209 -0
- package/src/context/context-retrieval-integration.js +60 -0
- package/src/context/context-retrieval.js +93 -0
- package/src/context/context-router.js +457 -0
- package/src/context/context-routing-evaluation.js +331 -0
- package/src/context/context-routing-observability.js +296 -0
- package/src/context/context-routing-shadow.js +237 -0
- package/src/context/context-scope.js +181 -0
- package/src/context/context-snapshot.js +226 -0
- package/src/context/context-source-registry.js +95 -0
- package/src/context/context-sufficiency-gate.js +120 -0
- package/src/context/context-sufficiency-oracle.js +167 -0
- package/src/context/context-telemetry.js +176 -0
- package/src/context/context.js +633 -0
- package/src/context/dependency-closure.js +246 -0
- package/src/context/page-fault-tracker.js +275 -0
- package/src/context/prompt-engine.js +74 -0
- package/src/context/runtime-context-graph.js +260 -0
- package/src/context/selection.js +2 -0
- package/src/context/session.js +2 -0
- package/src/context/sufficiency-contract.js +300 -0
- package/src/context/tokenizer.js +17 -0
- package/src/context-benchmark-router.mjs +125 -0
- package/src/evidence/evidence-engine.js +161 -0
- package/src/evidence/evidence-factory.js +3 -0
- package/src/evidence/evidence-freshness.js +463 -0
- package/src/evidence/evidence-gap.js +76 -0
- package/src/evidence/evidence-lineage.js +525 -0
- package/src/evidence/evidence.js +136 -0
- package/src/evidence/freshness-validation.js +187 -0
- package/src/evidence/lineage.js +4 -0
- package/src/execution/action-evaluation.js +179 -0
- package/src/execution/circuit-breaker.js +70 -0
- package/src/execution/claim-interception.js +63 -0
- package/src/execution/execution-engine.js +258 -0
- package/src/execution/execution-intent.js +69 -0
- package/src/execution/execution-state.js +89 -0
- package/src/execution/formatting.js +60 -0
- package/src/execution/l1-sanitize.js +16 -0
- package/src/execution/l2-resource-guards.js +19 -0
- package/src/execution/l3-invariant-checker.js +5 -0
- package/src/execution/l4-containment.js +6 -0
- package/src/execution/runtime-guards.js +106 -0
- package/src/execution/strategy-identity.js +162 -0
- package/src/execution/tool-interceptor.js +7 -0
- package/src/execution/tool-result-classifier.js +63 -0
- package/src/integration/canary-deploy.js +143 -0
- package/src/integration/collector.js +59 -0
- package/src/integration/error-recovery.js +6 -0
- package/src/integration/event-bus.js +66 -0
- package/src/integration/health-checker.js +57 -0
- package/src/integration/logger.js +39 -0
- package/src/integration/message-handler.js +611 -0
- package/src/integration/opencode-db.js +153 -0
- package/src/integration/orchestration.js +419 -0
- package/src/integration/part-provenance.js +65 -0
- package/src/integration/rate-limiter.js +55 -0
- package/src/integration/release-gate.js +336 -0
- package/src/integration/rollback-manager.js +164 -0
- package/src/integration/router-adapter.js +269 -0
- package/src/integration/telemetry.js +45 -0
- package/src/integration/update-checker.js +10 -0
- package/src/persistence/event-sourcing.js +75 -0
- package/src/persistence/memory.js +501 -0
- package/src/persistence/migration-runner.js +16 -0
- package/src/persistence/persistence-manager.js +39 -0
- package/src/persistence/transaction-log.js +157 -0
- package/src/policy/action-capabilities.js +116 -0
- package/src/policy/action-policy.js +4 -0
- package/src/policy/assessment-engine.js +104 -0
- package/src/policy/assumption-tracking.js +66 -0
- package/src/policy/budget-enforcer.js +7 -0
- package/src/policy/config-validator.js +102 -0
- package/src/policy/execution-assessment.js +142 -0
- package/src/policy/failure-classification.js +5 -0
- package/src/policy/feature-flags.js +65 -0
- package/src/policy/governance-enforcement.js +118 -0
- package/src/policy/locale.js +15 -0
- package/src/policy/multi-signer.js +151 -0
- package/src/policy/policy-state-machine.js +97 -0
- package/src/policy/ponytail-constraint.js +38 -0
- package/src/policy/project-inspector.js +93 -0
- package/src/policy/query-builder.js +14 -0
- package/src/policy/request-classifier.js +79 -0
- package/src/policy/risk-engine.js +266 -0
- package/src/policy/scope-enforcement.js +140 -0
- package/src/policy/secret-scanner.js +68 -0
- package/src/policy/skill-routing.js +104 -0
- package/src/policy/strategy-capabilities.js +211 -0
- package/src/policy/strategy.js +6 -0
- package/src/policy/uncertainty.js +117 -0
- package/src/shared/container.js +44 -0
- package/src/shared/decision-types.js +24 -0
- package/src/shared/simple-test.js +160 -0
- package/src/shared/wam-events.js +52 -0
- package/src/skills/engine.js +1705 -0
- package/src/skills/skill-registry.js +19 -0
- package/src/skills/skill-routing.js +340 -0
- package/src/state/lifecycle-manager.js +113 -0
- package/src/state/operational-state.js +198 -0
- package/src/state/projections.js +47 -0
- package/src/state/replay-engine.js +5 -0
- package/src/state/requirement-ownership.js +20 -0
- package/src/state/requirement-state.js +152 -0
- package/src/state/run-event-identity.js +172 -0
- package/src/state/state-engine.js +19 -0
- package/src/state/state-machine.js +119 -0
- package/src/state/state-schema.js +318 -0
- package/src/state/state-store.js +335 -0
- package/src/state/state-transition.js +84 -0
- package/src/state/task-dependencies.js +287 -0
- package/src/state/task-execution.js +217 -0
- package/src/state/task-lifecycle.js +59 -0
- package/src/state/task-runs.js +361 -0
- package/src/state/task-state-file.js +86 -0
- package/src/state/task-state.js +41 -0
- package/src/state/task-status.js +54 -0
- package/src/state/task-store.js +48 -0
- package/src/state/wam-state.js +148 -0
- package/src/verification/completion-gate-simple.js +8 -0
- package/src/verification/completion-gate.js +183 -0
- package/src/verification/contract.js +9 -0
- package/src/verification/contradiction-resolution.js +255 -0
- package/src/verification/diagnostic-engine.js +659 -0
- package/src/verification/false-completion-prevention.js +100 -0
- package/src/verification/hook-validator.js +6 -0
- package/src/verification/requirements.js +6 -0
- package/src/verification/verification-context.js +162 -0
- package/src/verification/verification-lifecycle.js +66 -0
- package/src/verification/verification-model.js +89 -0
- package/src/verification/verification-persistence.js +129 -0
- package/src/verification/verification-policy.js +127 -0
- package/src/verification/verification.js +415 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Router Dependency Closure — proper traversal without depth limits.
|
|
3
|
+
*
|
|
4
|
+
* Separates graph traversal from context admission.
|
|
5
|
+
* Traversal finds ALL dependencies; admission decides which enter the pack.
|
|
6
|
+
*
|
|
7
|
+
* Safety limits (for preventing explosion):
|
|
8
|
+
* - maxNodes: maximum nodes to collect
|
|
9
|
+
* - maxTraversalCost: maximum operations
|
|
10
|
+
* - cycle detection via visited set
|
|
11
|
+
*
|
|
12
|
+
* These are SAFETY limits, not semantic limits.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} TraversalOptions
|
|
17
|
+
* @property {number} [maxNodes=100] - Safety limit for node count
|
|
18
|
+
* @property {number} [maxCost=1000] - Safety limit for traversal operations
|
|
19
|
+
* @property {string[]} [edgeTypes] - Edge types to follow (default: all)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} TraversalResult
|
|
24
|
+
* @property {string[]} nodes - Collected node IDs
|
|
25
|
+
* @property {number} cost - Actual traversal cost
|
|
26
|
+
* @property {boolean} limitReached - Whether safety limit was hit
|
|
27
|
+
* @property {string} limitType - Which limit was reached (if any)
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Traverse upstream dependencies from a node.
|
|
32
|
+
* Follows depends_on and produces edges until:
|
|
33
|
+
* - no more edges
|
|
34
|
+
* - cycle detected
|
|
35
|
+
* - safety limit reached
|
|
36
|
+
*
|
|
37
|
+
* @param {Object} graph - Context graph
|
|
38
|
+
* @param {string} startNodeId - Starting node
|
|
39
|
+
* @param {TraversalOptions} options
|
|
40
|
+
* @returns {TraversalResult}
|
|
41
|
+
*/
|
|
42
|
+
export function traverseUpstream(graph, startNodeId, options = {}) {
|
|
43
|
+
const { maxNodes = 100, maxCost = 1000, edgeTypes } = options;
|
|
44
|
+
|
|
45
|
+
const visited = new Set();
|
|
46
|
+
const nodes = [];
|
|
47
|
+
let cost = 0;
|
|
48
|
+
|
|
49
|
+
const queue = [startNodeId];
|
|
50
|
+
|
|
51
|
+
while (queue.length > 0) {
|
|
52
|
+
cost++;
|
|
53
|
+
if (cost > maxCost) {
|
|
54
|
+
return {
|
|
55
|
+
nodes,
|
|
56
|
+
cost,
|
|
57
|
+
limitReached: true,
|
|
58
|
+
limitType: "maxCost",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (nodes.length >= maxNodes) {
|
|
63
|
+
return {
|
|
64
|
+
nodes,
|
|
65
|
+
cost,
|
|
66
|
+
limitReached: true,
|
|
67
|
+
limitType: "maxNodes",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const nodeId = queue.shift();
|
|
72
|
+
|
|
73
|
+
if (visited.has(nodeId)) continue;
|
|
74
|
+
visited.add(nodeId);
|
|
75
|
+
|
|
76
|
+
const node = graph.getNode(nodeId);
|
|
77
|
+
if (!node) continue;
|
|
78
|
+
|
|
79
|
+
nodes.push(nodeId);
|
|
80
|
+
|
|
81
|
+
// Follow depends_on edges (upstream)
|
|
82
|
+
const dependsOn = graph.getEdgesFromByType
|
|
83
|
+
? graph.getEdgesFromByType(nodeId, "depends_on")
|
|
84
|
+
: graph.getEdgesFrom(nodeId).filter((e) => e.type === "depends_on");
|
|
85
|
+
|
|
86
|
+
for (const edge of dependsOn) {
|
|
87
|
+
if (!visited.has(edge.to)) {
|
|
88
|
+
queue.push(edge.to);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Follow produces edges (reverse direction - who produces this node)
|
|
93
|
+
const producesEdges = graph.getEdgesToByType
|
|
94
|
+
? graph.getEdgesToByType(nodeId, "produces")
|
|
95
|
+
: graph.getEdgesTo(nodeId).filter((e) => e.type === "produces");
|
|
96
|
+
|
|
97
|
+
for (const edge of producesEdges) {
|
|
98
|
+
if (!visited.has(edge.from)) {
|
|
99
|
+
queue.push(edge.from);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
nodes,
|
|
106
|
+
cost,
|
|
107
|
+
limitReached: false,
|
|
108
|
+
limitType: null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Traverse downstream effects from a node.
|
|
114
|
+
* Follows produces and depends_on edges in forward direction.
|
|
115
|
+
*
|
|
116
|
+
* @param {Object} graph - Context graph
|
|
117
|
+
* @param {string} startNodeId - Starting node
|
|
118
|
+
* @param {TraversalOptions} options
|
|
119
|
+
* @returns {TraversalResult}
|
|
120
|
+
*/
|
|
121
|
+
export function traverseDownstream(graph, startNodeId, options = {}) {
|
|
122
|
+
const { maxNodes = 100, maxCost = 1000 } = options;
|
|
123
|
+
|
|
124
|
+
const visited = new Set();
|
|
125
|
+
const nodes = [];
|
|
126
|
+
let cost = 0;
|
|
127
|
+
|
|
128
|
+
const queue = [startNodeId];
|
|
129
|
+
|
|
130
|
+
while (queue.length > 0) {
|
|
131
|
+
cost++;
|
|
132
|
+
if (cost > maxCost) {
|
|
133
|
+
return { nodes, cost, limitReached: true, limitType: "maxCost" };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (nodes.length >= maxNodes) {
|
|
137
|
+
return { nodes, cost, limitReached: true, limitType: "maxNodes" };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const nodeId = queue.shift();
|
|
141
|
+
|
|
142
|
+
if (visited.has(nodeId)) continue;
|
|
143
|
+
visited.add(nodeId);
|
|
144
|
+
|
|
145
|
+
const node = graph.getNode(nodeId);
|
|
146
|
+
if (!node) continue;
|
|
147
|
+
|
|
148
|
+
nodes.push(nodeId);
|
|
149
|
+
|
|
150
|
+
// Follow produces edges (downstream)
|
|
151
|
+
const produces = graph.getEdgesFromByType
|
|
152
|
+
? graph.getEdgesFromByType(nodeId, "produces")
|
|
153
|
+
: graph.getEdgesFrom(nodeId).filter((e) => e.type === "produces");
|
|
154
|
+
|
|
155
|
+
for (const edge of produces) {
|
|
156
|
+
if (!visited.has(edge.to)) {
|
|
157
|
+
queue.push(edge.to);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Follow depends_on edges (reverse - who depends on this)
|
|
162
|
+
const dependsOnEdges = graph.getEdgesToByType
|
|
163
|
+
? graph.getEdgesToByType(nodeId, "depends_on")
|
|
164
|
+
: graph.getEdgesTo(nodeId).filter((e) => e.type === "depends_on");
|
|
165
|
+
|
|
166
|
+
for (const edge of dependsOnEdges) {
|
|
167
|
+
if (!visited.has(edge.from)) {
|
|
168
|
+
queue.push(edge.from);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
nodes,
|
|
175
|
+
cost,
|
|
176
|
+
limitReached: false,
|
|
177
|
+
limitType: null,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Compute full dependency closure for a set of nodes.
|
|
183
|
+
* Combines upstream and downstream traversal.
|
|
184
|
+
*
|
|
185
|
+
* @param {Object} graph - Context graph
|
|
186
|
+
* @param {string[]} nodeIds - Starting nodes
|
|
187
|
+
* @param {TraversalOptions} options
|
|
188
|
+
* @returns {TraversalResult}
|
|
189
|
+
*/
|
|
190
|
+
export function computeDependencyClosure(graph, nodeIds, options = {}) {
|
|
191
|
+
const visited = new Set();
|
|
192
|
+
const allNodes = [];
|
|
193
|
+
let totalCost = 0;
|
|
194
|
+
|
|
195
|
+
for (const nodeId of nodeIds) {
|
|
196
|
+
const upstream = traverseUpstream(graph, nodeId, options);
|
|
197
|
+
const downstream = traverseDownstream(graph, nodeId, options);
|
|
198
|
+
|
|
199
|
+
for (const id of [...upstream.nodes, ...downstream.nodes]) {
|
|
200
|
+
if (!visited.has(id)) {
|
|
201
|
+
visited.add(id);
|
|
202
|
+
allNodes.push(id);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
totalCost += upstream.cost + downstream.cost;
|
|
207
|
+
|
|
208
|
+
if (upstream.limitReached || downstream.limitReached) {
|
|
209
|
+
return {
|
|
210
|
+
nodes: allNodes,
|
|
211
|
+
cost: totalCost,
|
|
212
|
+
limitReached: true,
|
|
213
|
+
limitType: upstream.limitReached ? upstream.limitType : downstream.limitType,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
nodes: allNodes,
|
|
220
|
+
cost: totalCost,
|
|
221
|
+
limitReached: false,
|
|
222
|
+
limitType: null,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Check if traversal would hit safety limits.
|
|
228
|
+
* Useful for pre-flight checks.
|
|
229
|
+
*
|
|
230
|
+
* @param {Object} graph - Context graph
|
|
231
|
+
* @param {string} nodeId - Node to check
|
|
232
|
+
* @param {TraversalOptions} options
|
|
233
|
+
* @returns {{ safe: boolean, reason: string }}
|
|
234
|
+
*/
|
|
235
|
+
export function checkTraversalSafety(graph, nodeId, options = {}) {
|
|
236
|
+
const result = traverseUpstream(graph, nodeId, { ...options, maxNodes: 10, maxCost: 20 });
|
|
237
|
+
|
|
238
|
+
if (result.limitReached) {
|
|
239
|
+
return {
|
|
240
|
+
safe: false,
|
|
241
|
+
reason: `Traversal hits ${result.limitType} limit early (${result.cost} ops, ${result.nodes.length} nodes)`,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { safe: true, reason: "Traversal within safety bounds" };
|
|
246
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page-Fault Tracker (C06 — page-fault-measurement).
|
|
3
|
+
*
|
|
4
|
+
* Tracks context page-faults: when omitted context is later requested via
|
|
5
|
+
* `retrieveContext()` (i.e. the system has to fetch what it had previously
|
|
6
|
+
* chosen NOT to inject). Provides per-fault metrics: fetch count, tokens
|
|
7
|
+
* fetched, latency, and reason classification.
|
|
8
|
+
*
|
|
9
|
+
* Design:
|
|
10
|
+
* - createPageFaultTracker() returns a tracker with:
|
|
11
|
+
* recordOmission({id, tokens, reason}) — log that context was dropped
|
|
12
|
+
* recordFetch({id, tokens, latencyMs, reason}) — log a subsequent fetch
|
|
13
|
+
* recordFault(id, ...) — record a fault (alias)
|
|
14
|
+
* snapshot() — current aggregate stats
|
|
15
|
+
* reset() — clear all state
|
|
16
|
+
* faults() — list of recorded faults
|
|
17
|
+
* - measurePageFaults(tracker, fetchFn) — runs an async fetch closure
|
|
18
|
+
* against the tracker and returns the snapshot.
|
|
19
|
+
*
|
|
20
|
+
* Page fault definition: a fetch of an id that was previously omitted
|
|
21
|
+
* (recorded via recordOmission). If the id was never omitted, it's a
|
|
22
|
+
* regular fetch and is NOT counted as a fault.
|
|
23
|
+
*
|
|
24
|
+
* Reason taxonomy (extensible):
|
|
25
|
+
* - "scope-overflow" — was trimmed because budget overflowed
|
|
26
|
+
* - "relevance-threshold" — failed admission scoring threshold
|
|
27
|
+
* - "explicit-omission" — manually dropped by selector
|
|
28
|
+
* - "ttl-expired" — context had aged out
|
|
29
|
+
* - "unknown" — no omission reason recorded
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export const FAULT_REASONS = Object.freeze({
|
|
33
|
+
SCOPE_OVERFLOW: "scope-overflow",
|
|
34
|
+
RELEVANCE_THRESHOLD: "relevance-threshold",
|
|
35
|
+
EXPLICIT_OMISSION: "explicit-omission",
|
|
36
|
+
TTL_EXPIRED: "ttl-expired",
|
|
37
|
+
UNKNOWN: "unknown",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {Object} OmissionRecord
|
|
42
|
+
* @property {string} id
|
|
43
|
+
* @property {number} tokens
|
|
44
|
+
* @property {string} reason
|
|
45
|
+
* @property {number} timestamp
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {Object} FaultRecord
|
|
50
|
+
* @property {string} id
|
|
51
|
+
* @property {number} tokens
|
|
52
|
+
* @property {number} latencyMs
|
|
53
|
+
* @property {string} reason
|
|
54
|
+
* @property {number} timestamp
|
|
55
|
+
* @property {string} omissionReason
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @typedef {Object} PageFaultSnapshot
|
|
60
|
+
* @property {number} omissionCount
|
|
61
|
+
* @property {number} fetchCount
|
|
62
|
+
* @property {number} faultCount
|
|
63
|
+
* @property {number} tokensOmitted
|
|
64
|
+
* @property {number} tokensFetched
|
|
65
|
+
* @property {number} tokensRefetched - tokens fetched that were faults
|
|
66
|
+
* @property {number} totalLatencyMs
|
|
67
|
+
* @property {number} faultLatencyMs
|
|
68
|
+
* @property {Object} faultsByReason - {reason: count}
|
|
69
|
+
* @property {Object} omissionsByReason - {reason: count}
|
|
70
|
+
* @property {number} faultRate - faultCount / fetchCount (0 if no fetches)
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @typedef {Object} PageFaultTracker
|
|
75
|
+
* @property {string[]} _ids
|
|
76
|
+
* @property {(record: OmissionRecord) => void} recordOmission
|
|
77
|
+
* @property {(record: {id: string, tokens?: number, latencyMs?: number, reason?: string}) => FaultRecord|null} recordFetch
|
|
78
|
+
* @property {(id: string, opts?: object) => FaultRecord|null} recordFault
|
|
79
|
+
* @property {() => OmissionRecord[]} omissions
|
|
80
|
+
* @property {() => FaultRecord[]} faults
|
|
81
|
+
* @property {() => PageFaultSnapshot} snapshot
|
|
82
|
+
* @property {() => void} reset
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create a new page-fault tracker.
|
|
87
|
+
*
|
|
88
|
+
* @param {Object} [opts]
|
|
89
|
+
* @param {number} [opts.clock=() => Date.now()] - injectable clock for tests
|
|
90
|
+
* @returns {PageFaultTracker}
|
|
91
|
+
*/
|
|
92
|
+
export function createPageFaultTracker(opts = {}) {
|
|
93
|
+
const clock = typeof opts.clock === "function" ? opts.clock : () => Date.now();
|
|
94
|
+
|
|
95
|
+
/** @type {Map<string, OmissionRecord>} */
|
|
96
|
+
const omissions = new Map();
|
|
97
|
+
/** @type {FaultRecord[]} */
|
|
98
|
+
const faults = [];
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Record that context `id` was omitted (not injected into the prompt).
|
|
102
|
+
* Calling recordOmission twice for the same id updates the prior record.
|
|
103
|
+
*
|
|
104
|
+
* @param {OmissionRecord} record
|
|
105
|
+
*/
|
|
106
|
+
function recordOmission(record) {
|
|
107
|
+
if (!record || typeof record.id !== "string") {
|
|
108
|
+
throw new TypeError("recordOmission: record.id must be a string");
|
|
109
|
+
}
|
|
110
|
+
const entry = {
|
|
111
|
+
id: record.id,
|
|
112
|
+
tokens: typeof record.tokens === "number" ? record.tokens : 0,
|
|
113
|
+
reason: typeof record.reason === "string" ? record.reason : FAULT_REASONS.UNKNOWN,
|
|
114
|
+
timestamp: typeof record.timestamp === "number" ? record.timestamp : clock(),
|
|
115
|
+
};
|
|
116
|
+
omissions.set(entry.id, entry);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Record a fetch of context `id`. If `id` was previously omitted, this
|
|
121
|
+
* is a page fault and a FaultRecord is returned. Otherwise returns null.
|
|
122
|
+
*
|
|
123
|
+
* @param {{id: string, tokens?: number, latencyMs?: number, reason?: string, timestamp?: number}} record
|
|
124
|
+
* @returns {FaultRecord|null}
|
|
125
|
+
*/
|
|
126
|
+
function recordFetch(record) {
|
|
127
|
+
if (!record || typeof record.id !== "string") {
|
|
128
|
+
throw new TypeError("recordFetch: record.id must be a string");
|
|
129
|
+
}
|
|
130
|
+
const id = record.id;
|
|
131
|
+
const omission = omissions.get(id);
|
|
132
|
+
if (!omission) return null;
|
|
133
|
+
|
|
134
|
+
const fault = {
|
|
135
|
+
id,
|
|
136
|
+
tokens: typeof record.tokens === "number" ? record.tokens : omission.tokens,
|
|
137
|
+
latencyMs: typeof record.latencyMs === "number" ? record.latencyMs : 0,
|
|
138
|
+
reason: typeof record.reason === "string" ? record.reason : FAULT_REASONS.UNKNOWN,
|
|
139
|
+
timestamp: typeof record.timestamp === "number" ? record.timestamp : clock(),
|
|
140
|
+
omissionReason: omission.reason,
|
|
141
|
+
};
|
|
142
|
+
faults.push(fault);
|
|
143
|
+
return fault;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Convenience alias for recordFetch.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} id
|
|
150
|
+
* @param {{tokens?: number, latencyMs?: number, reason?: string}} [opts]
|
|
151
|
+
* @returns {FaultRecord|null}
|
|
152
|
+
*/
|
|
153
|
+
function recordFault(id, opts = {}) {
|
|
154
|
+
return recordFetch({ id, ...(opts || {}) });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function snapshot() {
|
|
158
|
+
let tokensOmitted = 0;
|
|
159
|
+
let tokensFetched = 0;
|
|
160
|
+
let tokensRefetched = 0;
|
|
161
|
+
let totalLatencyMs = 0;
|
|
162
|
+
let faultLatencyMs = 0;
|
|
163
|
+
/** @type {Object<string, number>} */
|
|
164
|
+
const faultsByReason = {};
|
|
165
|
+
/** @type {Object<string, number>} */
|
|
166
|
+
const omissionsByReason = {};
|
|
167
|
+
|
|
168
|
+
for (const o of omissions.values()) {
|
|
169
|
+
tokensOmitted += o.tokens;
|
|
170
|
+
omissionsByReason[o.reason] = (omissionsByReason[o.reason] || 0) + 1;
|
|
171
|
+
}
|
|
172
|
+
for (const f of faults) {
|
|
173
|
+
tokensRefetched += f.tokens;
|
|
174
|
+
faultLatencyMs += f.latencyMs;
|
|
175
|
+
totalLatencyMs += f.latencyMs;
|
|
176
|
+
faultsByReason[f.reason] = (faultsByReason[f.reason] || 0) + 1;
|
|
177
|
+
}
|
|
178
|
+
const fetchCount = faults.length; // only faults count as fetches in this metric
|
|
179
|
+
const faultCount = faults.length;
|
|
180
|
+
const faultRate = fetchCount === 0 ? 0 : faultCount / fetchCount;
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
omissionCount: omissions.size,
|
|
184
|
+
fetchCount,
|
|
185
|
+
faultCount,
|
|
186
|
+
tokensOmitted,
|
|
187
|
+
tokensFetched, // alias for tokensRefetched under fault-only mode
|
|
188
|
+
tokensRefetched,
|
|
189
|
+
totalLatencyMs,
|
|
190
|
+
faultLatencyMs,
|
|
191
|
+
faultsByReason,
|
|
192
|
+
omissionsByReason,
|
|
193
|
+
faultRate,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function reset() {
|
|
198
|
+
omissions.clear();
|
|
199
|
+
faults.length = 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
recordOmission,
|
|
204
|
+
recordFetch,
|
|
205
|
+
recordFault,
|
|
206
|
+
omissions: () => Array.from(omissions.values()),
|
|
207
|
+
faults: () => faults.slice(),
|
|
208
|
+
snapshot,
|
|
209
|
+
reset,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Run `fetchFn` against the tracker and return the resulting snapshot.
|
|
215
|
+
* Useful for instrumenting an existing retrieveContext() call.
|
|
216
|
+
*
|
|
217
|
+
* The fetchFn receives a wrapped retrieveContext that automatically calls
|
|
218
|
+
* recordFetch on every retrieval, attributing tokens/latency to faults when
|
|
219
|
+
* applicable.
|
|
220
|
+
*
|
|
221
|
+
* @param {PageFaultTracker} tracker
|
|
222
|
+
* @param {(retrieve: (id: string, opts?: object) => any) => Promise<any>|any} fetchFn
|
|
223
|
+
* @returns {Promise<PageFaultSnapshot>|PageFaultSnapshot}
|
|
224
|
+
*/
|
|
225
|
+
export function measurePageFaults(tracker, fetchFn) {
|
|
226
|
+
if (!tracker || typeof tracker.recordFetch !== "function") {
|
|
227
|
+
throw new TypeError("measurePageFaults: tracker required");
|
|
228
|
+
}
|
|
229
|
+
if (typeof fetchFn !== "function") {
|
|
230
|
+
throw new TypeError("measurePageFaults: fetchFn must be a function");
|
|
231
|
+
}
|
|
232
|
+
const wrapped = (id, opts = {}) => {
|
|
233
|
+
const start = (opts && typeof opts._start === "number") ? opts._start : Date.now();
|
|
234
|
+
const result = (opts && typeof opts._result === "object") ? opts._result : null;
|
|
235
|
+
const tokens = result && typeof result.tokens === "number"
|
|
236
|
+
? result.tokens
|
|
237
|
+
: (opts && typeof opts.tokens === "number" ? opts.tokens : 0);
|
|
238
|
+
const latencyMs = result && typeof result.latencyMs === "number"
|
|
239
|
+
? result.latencyMs
|
|
240
|
+
: Math.max(0, Date.now() - start);
|
|
241
|
+
return tracker.recordFetch({
|
|
242
|
+
id,
|
|
243
|
+
tokens,
|
|
244
|
+
latencyMs,
|
|
245
|
+
reason: (opts && opts.reason) || "unknown",
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
// NOTE: deliberately not async — returns snapshot directly.
|
|
249
|
+
// If fetchFn returns a Promise, the caller should await.
|
|
250
|
+
const out = fetchFn(wrapped);
|
|
251
|
+
if (out && typeof out.then === "function") {
|
|
252
|
+
return out.catch(() => {}).then(() => tracker.snapshot());
|
|
253
|
+
}
|
|
254
|
+
return tracker.snapshot();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Compute per-fault aggregates grouped by omission reason.
|
|
259
|
+
*
|
|
260
|
+
* @param {FaultRecord[]} records
|
|
261
|
+
* @returns {Object<string, {count: number, tokens: number, latencyMs: number}>}
|
|
262
|
+
*/
|
|
263
|
+
export function aggregateByReason(records) {
|
|
264
|
+
/** @type {Object<string, {count: number, tokens: number, latencyMs: number}>} */
|
|
265
|
+
const out = {};
|
|
266
|
+
if (!Array.isArray(records)) return out;
|
|
267
|
+
for (const r of records) {
|
|
268
|
+
const key = (r && r.omissionReason) || FAULT_REASONS.UNKNOWN;
|
|
269
|
+
if (!out[key]) out[key] = { count: 0, tokens: 0, latencyMs: 0 };
|
|
270
|
+
out[key].count += 1;
|
|
271
|
+
out[key].tokens += typeof r.tokens === "number" ? r.tokens : 0;
|
|
272
|
+
out[key].latencyMs += typeof r.latencyMs === "number" ? r.latencyMs : 0;
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { countTokens } from './tokenizer.js';
|
|
2
|
+
|
|
3
|
+
export function buildPrompt({ system, context, user }, options = {}) {
|
|
4
|
+
const maxTokens = options.maxTokens ?? Infinity;
|
|
5
|
+
const reservedForUser = options.reservedForUser ?? 0;
|
|
6
|
+
|
|
7
|
+
const parts = [];
|
|
8
|
+
if (system) parts.push({ role: 'system', content: system });
|
|
9
|
+
if (context) parts.push({ role: 'context', content: context });
|
|
10
|
+
if (user) parts.push({ role: 'user', content: user });
|
|
11
|
+
|
|
12
|
+
const contentFor = (p) => (typeof p.content === 'string' ? p.content : '');
|
|
13
|
+
|
|
14
|
+
// Calculate per-part token counts.
|
|
15
|
+
const withTokens = parts.map((p) => ({ ...p, tokens: countTokens(contentFor(p)) }));
|
|
16
|
+
const totalTokens = withTokens.reduce((s, p) => s + p.tokens, 0);
|
|
17
|
+
|
|
18
|
+
if (totalTokens <= maxTokens) {
|
|
19
|
+
return {
|
|
20
|
+
parts: withTokens.map((p) => ({ role: p.role, content: p.content })),
|
|
21
|
+
totalTokens,
|
|
22
|
+
truncated: false,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Truncate lowest-priority parts first; system has highest priority.
|
|
27
|
+
// Process in reverse priority order so system is preserved.
|
|
28
|
+
const priorities = ['user', 'context'];
|
|
29
|
+
const resultParts = withTokens.map((p) => ({
|
|
30
|
+
role: p.role,
|
|
31
|
+
content: p.content,
|
|
32
|
+
tokens: p.tokens,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
let currentTokens = totalTokens;
|
|
36
|
+
for (const role of priorities) {
|
|
37
|
+
if (currentTokens <= maxTokens) break;
|
|
38
|
+
const idx = resultParts.findIndex((p) => p.role === role);
|
|
39
|
+
if (idx === -1) continue;
|
|
40
|
+
const part = resultParts[idx];
|
|
41
|
+
const remainingBudget = maxTokens - (currentTokens - part.tokens);
|
|
42
|
+
const minForUser = role === 'user' ? reservedForUser : 0;
|
|
43
|
+
const allowed = Math.max(0, remainingBudget - minForUser);
|
|
44
|
+
if (allowed >= part.tokens) continue;
|
|
45
|
+
if (allowed === 0) {
|
|
46
|
+
part.content = '';
|
|
47
|
+
part.tokens = 0;
|
|
48
|
+
} else {
|
|
49
|
+
// Truncate content by approximate char ratio (chars ~= 4 chars per token).
|
|
50
|
+
const charBudget = allowed * 4;
|
|
51
|
+
const truncated = contentFor(part).slice(0, charBudget);
|
|
52
|
+
part.content = truncated + (contentFor(part).length > charBudget ? '...' : '');
|
|
53
|
+
part.tokens = countTokens(part.content);
|
|
54
|
+
}
|
|
55
|
+
currentTokens = resultParts.reduce((s, p) => s + p.tokens, 0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// If still over budget after user/context truncation, drop user content entirely.
|
|
59
|
+
// System is preserved whenever possible (last resort: empty if it alone exceeds budget).
|
|
60
|
+
if (currentTokens > maxTokens) {
|
|
61
|
+
const userIdx = resultParts.findIndex((p) => p.role === 'user');
|
|
62
|
+
if (userIdx !== -1) {
|
|
63
|
+
resultParts[userIdx].content = '';
|
|
64
|
+
resultParts[userIdx].tokens = 0;
|
|
65
|
+
currentTokens = resultParts.reduce((s, p) => s + p.tokens, 0);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
parts: resultParts.map((p) => ({ role: p.role, content: p.content })),
|
|
71
|
+
totalTokens: currentTokens,
|
|
72
|
+
truncated: true,
|
|
73
|
+
};
|
|
74
|
+
}
|