@ngockhoale/ukit 3.0.7 → 3.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -1
- package/manifests/documentation.yaml +11 -0
- package/package.json +1 -1
- package/scripts/audit/decision-coverage.mjs +29 -2
- package/scripts/bench/data-foundation.mjs +52 -3
- package/scripts/bench/decision-runtime-baseline.mjs +427 -0
- package/scripts/bench/decision-runtime-metrics.mjs +67 -0
- package/scripts/bench/decision-runtime-variant.mjs +626 -0
- package/scripts/bench/memory-ablation.mjs +495 -0
- package/scripts/bench/memory-baseline.mjs +596 -0
- package/scripts/bench/memory-bench.mjs +661 -0
- package/scripts/bench/memory-canary.mjs +321 -0
- package/scripts/bench/memory-corpus.mjs +354 -0
- package/scripts/bench/memory-gate.mjs +389 -0
- package/scripts/bench/memory-metrics.mjs +179 -0
- package/scripts/bench/parallel-agents.mjs +33 -11
- package/scripts/bench/recorder-overhead.mjs +204 -0
- package/scripts/bench/sqlite-spike.mjs +451 -0
- package/scripts/measure-decision-gateway.mjs +306 -0
- package/scripts/perf/audit-perf.mjs +35 -17
- package/src/bug/triageBug.js +4 -3
- package/src/cli/commands/memory.js +357 -63
- package/src/context/detectProjectContext.js +11 -1
- package/src/core/agentRuntime/adapters.js +254 -0
- package/src/core/agentRuntime/artifacts.js +192 -0
- package/src/core/agentRuntime/completionGate.js +176 -0
- package/src/core/agentRuntime/context.js +149 -0
- package/src/core/agentRuntime/contract.js +247 -0
- package/src/core/agentRuntime/diagnostics.js +244 -0
- package/src/core/agentRuntime/evaluation.js +163 -0
- package/src/core/agentRuntime/eventStore.js +404 -0
- package/src/core/agentRuntime/liveness.js +60 -0
- package/src/core/agentRuntime/planCompiler.js +322 -0
- package/src/core/agentRuntime/promotion.js +53 -0
- package/src/core/agentRuntime/qualityComparison.js +112 -0
- package/src/core/agentRuntime/recovery.js +266 -0
- package/src/core/agentRuntime/resourcePolicy.js +78 -0
- package/src/core/agentRuntime/runtimeSupport.js +237 -0
- package/src/core/agentRuntime/supervisor.js +565 -0
- package/src/core/agentRuntime/vmEngine.js +621 -0
- package/src/core/codeintel/analogy.js +3 -2
- package/src/core/experiments/dynamicWorkflow.js +17 -2
- package/src/core/fileOps.js +21 -3
- package/src/core/memory/deltaOverlays.js +75 -30
- package/src/core/memory/learningCandidates.js +93 -48
- package/src/core/memory/memoryFlags.js +83 -0
- package/src/core/memory/memoryFreshness.js +190 -0
- package/src/core/memory/memoryHit.js +144 -0
- package/src/core/memory/migrate.js +69 -189
- package/src/core/memory/migrateMapping.js +232 -0
- package/src/core/memory/mutateMemory.js +323 -0
- package/src/core/memory/policy.js +96 -0
- package/src/core/memory/projectIdentity.js +266 -0
- package/src/core/memory/recordIndex.js +178 -0
- package/src/core/memory/recordStore.js +133 -20
- package/src/core/memory/records.js +144 -6
- package/src/core/memory/retrieval.js +259 -125
- package/src/core/memory/store.js +16 -5
- package/src/core/memory/storeBackup.js +226 -0
- package/src/core/memory/storeV2.js +63 -26
- package/src/core/memory/storeV2Loader.js +30 -12
- package/src/core/memory/userMemory.js +38 -20
- package/src/core/memory/writeClassification.js +161 -0
- package/src/core/memory/writeGuard.js +129 -0
- package/src/core/observability/adapters/hookTelemetryAdapter.js +90 -0
- package/src/core/observability/analytics/cohorts.js +148 -0
- package/src/core/observability/analytics/storeDigest.js +163 -0
- package/src/core/observability/evaluation/experimentPlan.js +95 -0
- package/src/core/observability/evaluation/findings.js +99 -0
- package/src/core/observability/evaluation/optimizationKnowledge.js +10 -1
- package/src/core/observability/evaluation/perturbation.js +273 -0
- package/src/core/observability/evaluation/replay.js +7 -1
- package/src/core/observability/evaluation/scorecard.js +23 -3
- package/src/core/observability/rollout.js +11 -7
- package/src/core/observability/schema/compatibility.js +135 -0
- package/src/core/observability/schema/registry.js +99 -0
- package/src/core/observability/schema/validate.js +7 -0
- package/src/core/observability/support/import.js +53 -9
- package/src/core/observability/support/paths.js +13 -3
- package/src/core/observability/support/projector.js +148 -12
- package/src/core/output/index.js +12 -2
- package/src/core/runtimeConfig.js +83 -0
- package/src/core/runtimePaths.js +3 -0
- package/src/core/sensitiveValueScanner.js +40 -0
- package/src/core/token/index.js +40 -3
- package/src/decision/client.js +37 -13
- package/src/decision/protocol.js +1 -1
- package/src/decision/registry.js +5 -3
- package/src/decision/runtimeDecide.js +242 -0
- package/src/decision/runtimeFilter.js +150 -0
- package/src/decision/runtimeScheduler.js +239 -0
- package/src/index/buildIndex.js +13 -12
- package/src/index/queryIndex.js +35 -14
- package/src/index/relatedTests.js +50 -8
- package/src/index/resolveContext.js +9 -4
- package/src/manifest/selectItems.js +7 -3
- package/src/render/instructionRenderer.js +17 -5
- package/template_project/.claude/ukit/index/lib/index-core.mjs +94 -39
- package/template_project/.claude/ukit/index/route-task.mjs +121 -19
- package/template_project/.claude/ukit/index/unic-decision.mjs +28 -13
- package/template_project/.claude/ukit/runtime/memory-flags.mjs +51 -0
- package/template_project/.claude/ukit/runtime/memory-freshness.mjs +155 -0
- package/template_project/.claude/ukit/runtime/memory-policy.mjs +286 -0
- package/template_project/.claude/ukit/runtime/output-compression.mjs +3 -0
- package/template_project/.claude/ukit/runtime/reinject-context.mjs +145 -14
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// src/decision/runtimeDecide.js — pure (no I/O, no config of its own).
|
|
2
|
+
// C65 G3-FR03/FR04 — registered semantic decisions + bounded fallback.
|
|
3
|
+
//
|
|
4
|
+
// createRuntimeDecider({ client, registry?, confidenceFloor?, now? }) produces
|
|
5
|
+
// the `decideFn` runtimeScheduler expects: one ambiguous classified item →
|
|
6
|
+
// exactly one client.requestBatch → TypedDecision, or a typed conservative
|
|
7
|
+
// Fallback on every failure mode. The gateway is contacted ONLY for items
|
|
8
|
+
// classified 'model_decision' with a registered decisionKey — known signals
|
|
9
|
+
// never reach the model.
|
|
10
|
+
//
|
|
11
|
+
// Fallback contract (SPEC §4, frozen):
|
|
12
|
+
// { decidedBy:'fallback', code:
|
|
13
|
+
// 'timeout'|'invalid'|'unavailable'|'low_confidence'|'abstained',
|
|
14
|
+
// action: <conservative per decisionKey> }
|
|
15
|
+
// Fallback NEVER auto-approves a retry or an escalation-to-action and never
|
|
16
|
+
// throws.
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
createDecisionRegistry,
|
|
20
|
+
DECISION_REGISTRY,
|
|
21
|
+
} from './registry.js';
|
|
22
|
+
|
|
23
|
+
const DEFAULT_CONFIDENCE_FLOOR = 0.6;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Registered runtime.*.v1 semantic decisions (G3-FR03). Each entry conforms
|
|
27
|
+
* to validateEntry/DECISION_KEY_PATTERN; `instruction`/`candidates` ride
|
|
28
|
+
* along as entry metadata so the decider can build the wire question without
|
|
29
|
+
* a second source of truth. All ship at rolloutStage 'off'.
|
|
30
|
+
*/
|
|
31
|
+
export const RUNTIME_DECISIONS = Object.freeze([
|
|
32
|
+
{
|
|
33
|
+
decisionKey: 'runtime.stall_action.v1',
|
|
34
|
+
schemaVersion: 1,
|
|
35
|
+
family: 'runtime',
|
|
36
|
+
owner: 'runtimeScheduler',
|
|
37
|
+
kind: 'choice',
|
|
38
|
+
description: 'Action for an ambiguous stalled/timeout observation without explicit spec policy.',
|
|
39
|
+
candidatePolicy: 'conservative-stall-actions',
|
|
40
|
+
hardConstraints: ['no-auto-kill'],
|
|
41
|
+
probabilityPolicy: 'raw-label',
|
|
42
|
+
fallbackPolicy: 'frozen-conservative-fallback',
|
|
43
|
+
telemetryClass: 'decision',
|
|
44
|
+
rolloutStage: 'off',
|
|
45
|
+
cacheSensitivity: 'none',
|
|
46
|
+
instruction:
|
|
47
|
+
'A runtime operation reported a stalled or timeout observation with no explicit spec policy. Choose the next action.',
|
|
48
|
+
candidates: ['continue_observe', 'request_nudge', 'escalate'],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
decisionKey: 'runtime.wake_needed.v1',
|
|
52
|
+
schemaVersion: 1,
|
|
53
|
+
family: 'runtime',
|
|
54
|
+
owner: 'runtimeScheduler',
|
|
55
|
+
kind: 'choice',
|
|
56
|
+
description: 'Whether an ambiguous runtime event needs to wake a host/human, from an unparseable or unknown payload.',
|
|
57
|
+
candidatePolicy: 'conservative-wake-actions',
|
|
58
|
+
hardConstraints: ['no-silent-drop'],
|
|
59
|
+
probabilityPolicy: 'raw-label',
|
|
60
|
+
fallbackPolicy: 'frozen-conservative-fallback',
|
|
61
|
+
telemetryClass: 'decision',
|
|
62
|
+
rolloutStage: 'off',
|
|
63
|
+
cacheSensitivity: 'none',
|
|
64
|
+
instruction:
|
|
65
|
+
'A runtime event arrived with an unparseable or unrecognized payload. Decide whether to keep observing or escalate.',
|
|
66
|
+
candidates: ['continue_observe', 'escalate'],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
decisionKey: 'runtime.safe_retry.v1',
|
|
70
|
+
schemaVersion: 1,
|
|
71
|
+
family: 'runtime',
|
|
72
|
+
owner: 'runtimeScheduler',
|
|
73
|
+
kind: 'choice',
|
|
74
|
+
description: 'Whether an ambiguous failure-adjacent event is safe to retry within registered bounds.',
|
|
75
|
+
candidatePolicy: 'conservative-retry-actions',
|
|
76
|
+
hardConstraints: ['no-auto-retry-approval'],
|
|
77
|
+
probabilityPolicy: 'raw-label',
|
|
78
|
+
fallbackPolicy: 'frozen-conservative-fallback',
|
|
79
|
+
telemetryClass: 'decision',
|
|
80
|
+
rolloutStage: 'off',
|
|
81
|
+
cacheSensitivity: 'none',
|
|
82
|
+
instruction:
|
|
83
|
+
'A runtime event may warrant a retry but no deterministic policy covers it. Decide conservatively.',
|
|
84
|
+
candidates: ['retry', 'no_retry', 'escalate'],
|
|
85
|
+
},
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
// Decision→fallback action map (frozen, SPEC §4). Fallbacks are conservative:
|
|
89
|
+
// a stall keeps observing (never auto-kills), an unknown retry does not retry,
|
|
90
|
+
// an ambiguous wake escalates to a human boundary.
|
|
91
|
+
const FALLBACK_ACTIONS = Object.freeze({
|
|
92
|
+
'runtime.stall_action.v1': 'continue_observe',
|
|
93
|
+
'runtime.wake_needed.v1': 'escalate',
|
|
94
|
+
'runtime.safe_retry.v1': 'no_retry',
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Batch-level statuses → fallback codes. 'abstained' stays distinct so
|
|
98
|
+
// shadow-agreement telemetry can separate model-refusal from transport
|
|
99
|
+
// failure; everything else folds into the nearest conservative class.
|
|
100
|
+
const STATUS_CODES = Object.freeze({
|
|
101
|
+
timeout: 'timeout',
|
|
102
|
+
invalid: 'invalid',
|
|
103
|
+
abstained: 'abstained',
|
|
104
|
+
unavailable: 'unavailable',
|
|
105
|
+
unsupported: 'unavailable',
|
|
106
|
+
'blocked-sensitive': 'invalid',
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
function isPlainObject(value) {
|
|
110
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function fallback(code, action) {
|
|
114
|
+
return { decidedBy: 'fallback', code, action };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function fallbackActionFor(decisionKey) {
|
|
118
|
+
return FALLBACK_ACTIONS[decisionKey] ?? 'escalate';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function resolveFloor(confidenceFloor, decisionKey) {
|
|
122
|
+
if (isPlainObject(confidenceFloor)) {
|
|
123
|
+
const perKey = confidenceFloor[decisionKey];
|
|
124
|
+
return Number.isFinite(perKey) ? perKey : DEFAULT_CONFIDENCE_FLOOR;
|
|
125
|
+
}
|
|
126
|
+
return Number.isFinite(confidenceFloor) ? confidenceFloor : DEFAULT_CONFIDENCE_FLOOR;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Minimal state packet — only the fields the model needs; the full event
|
|
130
|
+
// payload stays out of the request (budget + sensitive-value gates live in
|
|
131
|
+
// the client).
|
|
132
|
+
function buildStatePacket(item) {
|
|
133
|
+
return {
|
|
134
|
+
stateVersion: 1,
|
|
135
|
+
decisionKey: item.decisionKey ?? null,
|
|
136
|
+
reason: item.reason ?? null,
|
|
137
|
+
eventType: item.event?.eventType ?? item.eventType ?? null,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function buildQuestion(item, entry) {
|
|
142
|
+
return {
|
|
143
|
+
decisionKey: entry.decisionKey,
|
|
144
|
+
schemaVersion: entry.schemaVersion ?? 1,
|
|
145
|
+
kind: entry.kind,
|
|
146
|
+
instruction:
|
|
147
|
+
typeof entry.instruction === 'string' && entry.instruction.length > 0
|
|
148
|
+
? entry.instruction
|
|
149
|
+
: `Decide ${entry.decisionKey} for the described runtime event.`,
|
|
150
|
+
candidates: Array.isArray(entry.candidates) ? entry.candidates : [],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {{client: {requestBatch: Function}, registry?: object,
|
|
156
|
+
* confidenceFloor?: number|object, now?: Function}} options
|
|
157
|
+
* registry: defaults to DECISION_REGISTRY composed with RUNTIME_DECISIONS —
|
|
158
|
+
* the shipped catalog is frozen, so composition copies, never mutates.
|
|
159
|
+
* @returns {{decide: (item: object) => Promise<object>}}
|
|
160
|
+
*/
|
|
161
|
+
export function createRuntimeDecider({
|
|
162
|
+
client,
|
|
163
|
+
registry,
|
|
164
|
+
confidenceFloor,
|
|
165
|
+
now = () => Date.now(),
|
|
166
|
+
} = {}) {
|
|
167
|
+
const resolvedRegistry =
|
|
168
|
+
registry ?? createDecisionRegistry([...DECISION_REGISTRY, ...RUNTIME_DECISIONS]);
|
|
169
|
+
void now; // injectable for parity with client/scheduler; no clock use yet.
|
|
170
|
+
|
|
171
|
+
async function decide(item) {
|
|
172
|
+
if (!isPlainObject(item)) {
|
|
173
|
+
return fallback('invalid', 'escalate');
|
|
174
|
+
}
|
|
175
|
+
// Only 'model_decision' items reach the gateway — known local signals are
|
|
176
|
+
// decided upstream by runtimeFilter and pass through untouched.
|
|
177
|
+
if (item.action !== 'model_decision') {
|
|
178
|
+
return item;
|
|
179
|
+
}
|
|
180
|
+
const decisionKey = item.decisionKey;
|
|
181
|
+
const conservative = fallbackActionFor(decisionKey);
|
|
182
|
+
const entry =
|
|
183
|
+
typeof decisionKey === 'string' ? resolvedRegistry.getDecision(decisionKey) : null;
|
|
184
|
+
if (!entry) {
|
|
185
|
+
// Unregistered/unknown key — never send an unregistered question to the
|
|
186
|
+
// gateway; conservative fallback instead.
|
|
187
|
+
return fallback('invalid', conservative);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let result;
|
|
191
|
+
try {
|
|
192
|
+
result = await client.requestBatch({
|
|
193
|
+
batchId: item.batchId ?? null,
|
|
194
|
+
boundary: 'runtime',
|
|
195
|
+
statePacket: buildStatePacket(item),
|
|
196
|
+
questions: [buildQuestion(item, entry)],
|
|
197
|
+
});
|
|
198
|
+
} catch {
|
|
199
|
+
// A throwing client never propagates — SPEC §5: decideFn throws →
|
|
200
|
+
// 'invalid', scheduler stays alive.
|
|
201
|
+
return fallback('invalid', conservative);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const statusCode = STATUS_CODES[result?.status];
|
|
205
|
+
if (statusCode !== undefined) {
|
|
206
|
+
return fallback(statusCode, conservative);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const answer = Array.isArray(result?.answers)
|
|
210
|
+
? result.answers.find(
|
|
211
|
+
(a) => a?.decisionKey === decisionKey && a?.validationStatus === 'valid',
|
|
212
|
+
)
|
|
213
|
+
: null;
|
|
214
|
+
if (!answer) {
|
|
215
|
+
return fallback('invalid', conservative);
|
|
216
|
+
}
|
|
217
|
+
// Missing confidence is treated as low confidence — never auto-approve.
|
|
218
|
+
const confidence = Number.isFinite(answer.confidence) ? answer.confidence : 0;
|
|
219
|
+
if (confidence < resolveFloor(confidenceFloor, decisionKey)) {
|
|
220
|
+
return fallback('low_confidence', conservative);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
decidedBy: 'model',
|
|
225
|
+
decisionKey,
|
|
226
|
+
action: answer.value,
|
|
227
|
+
value: answer.value,
|
|
228
|
+
kind: answer.kind ?? entry.kind,
|
|
229
|
+
confidence: answer.confidence,
|
|
230
|
+
probabilities: answer.probabilities ?? null,
|
|
231
|
+
calibration: answer.calibration ?? 'unknown',
|
|
232
|
+
checkpoint: result?.checkpoint ?? null,
|
|
233
|
+
batchId: result?.batchId ?? null,
|
|
234
|
+
requestFingerprint: result?.requestFingerprint ?? null,
|
|
235
|
+
latencyClass: result?.latencyClass ?? null,
|
|
236
|
+
circuitState: result?.circuitState ?? null,
|
|
237
|
+
finishReason: result?.finishReason ?? null,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return { decide };
|
|
242
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// src/decision/runtimeFilter.js — pure (no I/O, no config, no gateway).
|
|
2
|
+
// C65 G3-FR01 — deterministic pre-filter for G1/G2 SemanticEvents.
|
|
3
|
+
// Known signals are decided locally and must NEVER produce a model call
|
|
4
|
+
// downstream; only genuinely ambiguous items return action 'model_decision'
|
|
5
|
+
// with a registered `runtime.*.v1` decisionKey.
|
|
6
|
+
//
|
|
7
|
+
// Interface (SPEC §4, frozen):
|
|
8
|
+
// classifyRuntimeEvent(event, policy?) -> {
|
|
9
|
+
// action: 'local_transition'|'local_observation'|'local_action'
|
|
10
|
+
// |'model_decision'|'escalate'|'ignore',
|
|
11
|
+
// priority: 'critical'|'decision'|'progress',
|
|
12
|
+
// coalesceKey?: string, // only for coalescible progress
|
|
13
|
+
// decisionKey?: string, // only when action='model_decision'
|
|
14
|
+
// reason: string }
|
|
15
|
+
//
|
|
16
|
+
// Consumes: SemanticEvent { eventType, safePayload, operationId, seq, ... }
|
|
17
|
+
// from src/core/agentRuntime/contract.js; liveness class vocabulary from
|
|
18
|
+
// src/core/agentRuntime/liveness.js.
|
|
19
|
+
// Consumed by: runtimeScheduler.submit (TASK-002), runtimeDecide (TASK-003).
|
|
20
|
+
|
|
21
|
+
import { TERMINAL_STATES } from '../core/agentRuntime/contract.js';
|
|
22
|
+
import { LIVENESS_CLASSES } from '../core/agentRuntime/liveness.js';
|
|
23
|
+
|
|
24
|
+
const TERMINAL_SET = new Set(TERMINAL_STATES);
|
|
25
|
+
const LIVENESS_SET = new Set(LIVENESS_CLASSES);
|
|
26
|
+
|
|
27
|
+
// Observation classes that represent redundant progress — safe to coalesce
|
|
28
|
+
// by key, latest wins (SPEC §5).
|
|
29
|
+
const COALESCIBLE_CLASSES = new Set(['live_progressing', 'silent_live']);
|
|
30
|
+
|
|
31
|
+
// Observation classes where a stall decision may be needed: with an explicit
|
|
32
|
+
// spec/policy stallAction the action is deterministic; without one the item
|
|
33
|
+
// is ambiguous and goes to the model via runtime.stall_action.v1.
|
|
34
|
+
const STALL_CLASSES = new Set(['stalled', 'timeout']);
|
|
35
|
+
|
|
36
|
+
// Ambiguous items that need a semantic decision: which decision key each
|
|
37
|
+
// maps to. Keys must be registered in the decision registry (TASK-003).
|
|
38
|
+
const DECISION_KEYS = Object.freeze({
|
|
39
|
+
stall: 'runtime.stall_action.v1',
|
|
40
|
+
ambiguous: 'runtime.wake_needed.v1',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Signals that a human/authorization boundary must resolve — never dropped,
|
|
44
|
+
// never coalesced, never auto-acted-on.
|
|
45
|
+
const ESCALATION_EVENT_TYPE = /permission|input[_-]?required|authoriz/i;
|
|
46
|
+
const ESCALATION_CLASS = /permission|input[_-]?required|authoriz|approval/i;
|
|
47
|
+
|
|
48
|
+
function result(action, priority, reason, extra) {
|
|
49
|
+
return { action, priority, reason, ...extra };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ignore = (reason) => result('ignore', 'progress', reason);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Classify one runtime SemanticEvent into a local action, a model-decision
|
|
56
|
+
* candidate, an escalation, or ignore. Pure: no I/O, no config import.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} event G1 SemanticEvent envelope
|
|
59
|
+
* @param {object} [policy] { stallAction?: string } — explicit spec policy
|
|
60
|
+
* @returns {{action:string,priority:string,coalesceKey?:string,
|
|
61
|
+
* decisionKey?:string,reason:string}}
|
|
62
|
+
*/
|
|
63
|
+
export function classifyRuntimeEvent(event, policy) {
|
|
64
|
+
if (event === null || typeof event !== 'object' || Array.isArray(event)) {
|
|
65
|
+
return ignore('malformed_event');
|
|
66
|
+
}
|
|
67
|
+
const eventType = event.eventType;
|
|
68
|
+
if (typeof eventType !== 'string' || eventType.length === 0) {
|
|
69
|
+
return ignore('malformed_event');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Permission / user-input / authorization signals escalate deterministically,
|
|
73
|
+
// regardless of the channel they arrive on.
|
|
74
|
+
if (ESCALATION_EVENT_TYPE.test(eventType)) {
|
|
75
|
+
return result('escalate', 'critical', `escalation_signal:${eventType}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const safePayload = event.safePayload;
|
|
79
|
+
const operationId =
|
|
80
|
+
typeof event.operationId === 'string' && event.operationId.length > 0
|
|
81
|
+
? event.operationId
|
|
82
|
+
: 'unknown';
|
|
83
|
+
|
|
84
|
+
switch (eventType) {
|
|
85
|
+
case 'operation.transition': {
|
|
86
|
+
if (typeof safePayload !== 'object' || safePayload === null) {
|
|
87
|
+
return result('model_decision', 'decision', 'unparseable_payload', {
|
|
88
|
+
decisionKey: DECISION_KEYS.ambiguous,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const to = safePayload.to;
|
|
92
|
+
if (TERMINAL_SET.has(to)) {
|
|
93
|
+
return result('local_transition', 'critical', `terminal_transition:${to}`);
|
|
94
|
+
}
|
|
95
|
+
// Non-terminal transition (queued→running, recovery_required, …):
|
|
96
|
+
// known signal, decided locally; never reaches the model.
|
|
97
|
+
return result('local_transition', 'progress', `transition:${to ?? 'unknown'}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
case 'operation.observation': {
|
|
101
|
+
if (typeof safePayload !== 'object' || safePayload === null) {
|
|
102
|
+
return result('model_decision', 'decision', 'unparseable_payload', {
|
|
103
|
+
decisionKey: DECISION_KEYS.ambiguous,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const cls = safePayload.class;
|
|
107
|
+
|
|
108
|
+
if (typeof cls === 'string' && ESCALATION_CLASS.test(cls)) {
|
|
109
|
+
return result('escalate', 'critical', `escalation_signal:${cls}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (COALESCIBLE_CLASSES.has(cls)) {
|
|
113
|
+
return result('local_observation', 'progress', `observation:${cls}`, {
|
|
114
|
+
coalesceKey: `obs:${operationId}:${cls}`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (STALL_CLASSES.has(cls)) {
|
|
119
|
+
const stallAction =
|
|
120
|
+
policy && typeof policy === 'object' && typeof policy.stallAction === 'string'
|
|
121
|
+
? policy.stallAction
|
|
122
|
+
: null;
|
|
123
|
+
if (stallAction) {
|
|
124
|
+
return result('local_action', 'critical', `policy_stall_action:${stallAction}:${cls}`);
|
|
125
|
+
}
|
|
126
|
+
return result('model_decision', 'decision', `ambiguous_stall:${cls}`, {
|
|
127
|
+
decisionKey: DECISION_KEYS.stall,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (LIVENESS_SET.has(cls)) {
|
|
132
|
+
// 'exited' — the only remaining liveness class. Terminal-adjacent
|
|
133
|
+
// sample signal: critical so it is never dropped/starved by progress.
|
|
134
|
+
return result('local_observation', 'critical', `observation:${cls}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Unrecognized observation class — ambiguous.
|
|
138
|
+
return result('model_decision', 'decision', `unknown_observation_class:${cls ?? 'none'}`, {
|
|
139
|
+
decisionKey: DECISION_KEYS.ambiguous,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
default:
|
|
144
|
+
// Valid envelope shape but a vocabulary the filter does not own —
|
|
145
|
+
// ambiguous, candidate for a semantic decision.
|
|
146
|
+
return result('model_decision', 'decision', `unknown_event_type:${eventType}`, {
|
|
147
|
+
decisionKey: DECISION_KEYS.ambiguous,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runtimeScheduler.js (G3-FR02)
|
|
3
|
+
*
|
|
4
|
+
* Bounded 3-tier queue for classified runtime items (SPEC §4):
|
|
5
|
+
* - critical : terminal/failure/cancel/permission — emitted directly via
|
|
6
|
+
* emitFn at flush; never dropped, never coalesced, never
|
|
7
|
+
* counted against maxQueue, always drained first.
|
|
8
|
+
* - decision : ambiguous items awaiting decideFn.
|
|
9
|
+
* - progress : redundant progress — coalesced by coalesceKey while an
|
|
10
|
+
* entry is pending (latest wins); eligible for drain after
|
|
11
|
+
* coalesceWindowMs from first arrival.
|
|
12
|
+
*
|
|
13
|
+
* Serial drain (maxInFlight = 1 — the gateway is a single local model).
|
|
14
|
+
* decideFn failures convert to typed Fallback objects emitted via emitFn;
|
|
15
|
+
* a decideFn that never resolves yields { code:'timeout' }.
|
|
16
|
+
*
|
|
17
|
+
* Pure module: no I/O, no config. All dependencies injected via opts.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { resolveConfigStage } from '../core/runtimeConfig.js';
|
|
21
|
+
import { selectResourcePolicy } from '../core/agentRuntime/resourcePolicy.js';
|
|
22
|
+
|
|
23
|
+
// G6 (DR-09): `decisionRuntime.resourcePolicy.stage` — absent/malformed →
|
|
24
|
+
// 'off'; 'shadow' consults selectResourcePolicy on submit and records a
|
|
25
|
+
// bounded receipt WITHOUT changing enqueue behavior. A policy throw or
|
|
26
|
+
// malformed decision deterministically falls back to the prior path.
|
|
27
|
+
const RESOURCE_FLAG = 'decisionRuntime.resourcePolicy.stage';
|
|
28
|
+
const RESOURCE_RECEIPT_CAP = 128;
|
|
29
|
+
|
|
30
|
+
// priority → eventClass for the resource consult. Scheduler-internal,
|
|
31
|
+
// fail-open: unknown priorities consult as 'progress' (the only coalescible
|
|
32
|
+
// class); malformed input still resolves emit downstream.
|
|
33
|
+
const EVENT_CLASS_BY_PRIORITY = Object.freeze({
|
|
34
|
+
critical: 'terminal',
|
|
35
|
+
progress: 'progress',
|
|
36
|
+
decision: 'progress',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const DEFAULT_COALESCE_WINDOW_MS = 250;
|
|
40
|
+
const DEFAULT_MAX_QUEUE = 256;
|
|
41
|
+
const DEFAULT_DECISION_TIMEOUT_MS = 5000;
|
|
42
|
+
|
|
43
|
+
function fallback(code, item) {
|
|
44
|
+
return { decidedBy: 'fallback', code, action: item?.action ?? 'ignore' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createScheduler(opts = {}) {
|
|
48
|
+
const decideFn = opts.decideFn ?? (async () => fallback('unavailable'));
|
|
49
|
+
const emitFn = opts.emitFn ?? (() => {});
|
|
50
|
+
const now = opts.now ?? (() => Date.now());
|
|
51
|
+
const coalesceWindowMs = opts.coalesceWindowMs ?? DEFAULT_COALESCE_WINDOW_MS;
|
|
52
|
+
const maxQueue = opts.maxQueue ?? DEFAULT_MAX_QUEUE;
|
|
53
|
+
const decisionTimeoutMs = opts.decisionTimeoutMs ?? DEFAULT_DECISION_TIMEOUT_MS;
|
|
54
|
+
// maxInFlight accepted for forward-compat; drain is serial regardless.
|
|
55
|
+
void (opts.maxInFlight ?? 1);
|
|
56
|
+
|
|
57
|
+
const resourceStage = () => resolveConfigStage(opts.config ?? null, RESOURCE_FLAG);
|
|
58
|
+
const resourcePolicyFn = typeof opts.resourcePolicyFn === 'function'
|
|
59
|
+
? opts.resourcePolicyFn
|
|
60
|
+
: selectResourcePolicy;
|
|
61
|
+
const resourcesFn = typeof opts.resourcesFn === 'function'
|
|
62
|
+
? opts.resourcesFn
|
|
63
|
+
: () => ({});
|
|
64
|
+
const resourceReceipts = [];
|
|
65
|
+
|
|
66
|
+
function recordResourceReceipt(entry) {
|
|
67
|
+
if (resourceReceipts.length >= RESOURCE_RECEIPT_CAP) resourceReceipts.shift();
|
|
68
|
+
resourceReceipts.push(Object.freeze(entry));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Shadow consult: evaluate the resource policy for this submit and record
|
|
73
|
+
* a bounded receipt. Never changes enqueue behavior; never throws.
|
|
74
|
+
*/
|
|
75
|
+
function consultResourcePolicy(item) {
|
|
76
|
+
let decision;
|
|
77
|
+
let error;
|
|
78
|
+
try {
|
|
79
|
+
const resources = resourcesFn() ?? {};
|
|
80
|
+
decision = resourcePolicyFn({
|
|
81
|
+
cpuPressure: typeof resources.cpuPressure === 'string' ? resources.cpuPressure : 'normal',
|
|
82
|
+
eventClass: EVENT_CLASS_BY_PRIORITY[item?.priority] ?? 'progress',
|
|
83
|
+
queueDepth: boundedSize(),
|
|
84
|
+
});
|
|
85
|
+
} catch (err) {
|
|
86
|
+
error = String(err?.message ?? err);
|
|
87
|
+
}
|
|
88
|
+
const valid = decision !== null && typeof decision === 'object'
|
|
89
|
+
&& (decision.action === 'emit' || decision.action === 'coalesce' || decision.action === 'defer');
|
|
90
|
+
recordResourceReceipt({
|
|
91
|
+
priority: item?.priority ?? null,
|
|
92
|
+
stage: resourceStage(),
|
|
93
|
+
decision: valid ? { action: decision.action, reason: decision.reason } : null,
|
|
94
|
+
error: error ?? null,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const critical = [];
|
|
99
|
+
const decision = [];
|
|
100
|
+
const progressPending = new Map(); // coalesceKey -> { item, firstArrival }
|
|
101
|
+
const timers = new Set();
|
|
102
|
+
let closed = false;
|
|
103
|
+
let closedWith = 0;
|
|
104
|
+
const counters = { submitted: 0, coalesced: 0, dropped: 0, deferred: 0, emitted: 0 };
|
|
105
|
+
|
|
106
|
+
function boundedSize() {
|
|
107
|
+
return decision.length + progressPending.size;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function submit(item) {
|
|
111
|
+
if (closed || !item || typeof item !== 'object') {
|
|
112
|
+
return { accepted: false, code: closed ? 'closed' : 'invalid' };
|
|
113
|
+
}
|
|
114
|
+
// G6 resource consult — shadow evaluates+records a bounded receipt;
|
|
115
|
+
// enqueue below is always the prior path (throw/malformed → prior
|
|
116
|
+
// behavior deterministically).
|
|
117
|
+
if (resourceStage() !== 'off') {
|
|
118
|
+
consultResourcePolicy(item);
|
|
119
|
+
}
|
|
120
|
+
if (item.priority === 'critical') {
|
|
121
|
+
critical.push(item);
|
|
122
|
+
counters.submitted++;
|
|
123
|
+
return { accepted: true };
|
|
124
|
+
}
|
|
125
|
+
if (item.priority === 'progress' && item.coalesceKey) {
|
|
126
|
+
const existing = progressPending.get(item.coalesceKey);
|
|
127
|
+
if (existing) {
|
|
128
|
+
existing.item = item; // latest wins
|
|
129
|
+
counters.coalesced++;
|
|
130
|
+
counters.submitted++;
|
|
131
|
+
return { accepted: true };
|
|
132
|
+
}
|
|
133
|
+
if (boundedSize() >= maxQueue) {
|
|
134
|
+
counters.dropped++;
|
|
135
|
+
return { accepted: false, code: 'queue_full' };
|
|
136
|
+
}
|
|
137
|
+
progressPending.set(item.coalesceKey, { item, firstArrival: now() });
|
|
138
|
+
counters.submitted++;
|
|
139
|
+
return { accepted: true };
|
|
140
|
+
}
|
|
141
|
+
// decision tier (or progress without a key — cannot coalesce)
|
|
142
|
+
if (boundedSize() >= maxQueue) {
|
|
143
|
+
counters.dropped++;
|
|
144
|
+
return { accepted: false, code: 'queue_full' };
|
|
145
|
+
}
|
|
146
|
+
decision.push(item);
|
|
147
|
+
counters.submitted++;
|
|
148
|
+
return { accepted: true };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function scheduleTimeout(ms) {
|
|
152
|
+
let handle;
|
|
153
|
+
const p = new Promise((resolve) => {
|
|
154
|
+
handle = setTimeout(() => {
|
|
155
|
+
timers.delete(handle);
|
|
156
|
+
resolve('timeout');
|
|
157
|
+
}, ms);
|
|
158
|
+
if (typeof handle.unref === 'function') handle.unref();
|
|
159
|
+
});
|
|
160
|
+
timers.add(handle);
|
|
161
|
+
return { promise: p, cancel: () => { clearTimeout(handle); timers.delete(handle); } };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function decide(item) {
|
|
165
|
+
const t = scheduleTimeout(decisionTimeoutMs);
|
|
166
|
+
try {
|
|
167
|
+
const result = await Promise.race([Promise.resolve().then(() => decideFn(item)), t.promise]);
|
|
168
|
+
t.cancel();
|
|
169
|
+
if (result === 'timeout') return fallback('timeout', item);
|
|
170
|
+
if (result && result.decidedBy === 'fallback') return result;
|
|
171
|
+
if (result === undefined || result === null) return fallback('invalid', item);
|
|
172
|
+
return result;
|
|
173
|
+
} catch {
|
|
174
|
+
t.cancel();
|
|
175
|
+
return fallback('invalid', item);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function flush() {
|
|
180
|
+
const report = { processed: 0, emitted: 0, coalesced: 0, dropped: 0, deferred: 0 };
|
|
181
|
+
if (closed) return report;
|
|
182
|
+
|
|
183
|
+
while (critical.length) {
|
|
184
|
+
const item = critical.shift();
|
|
185
|
+
emitFn(item);
|
|
186
|
+
counters.emitted++;
|
|
187
|
+
report.processed++;
|
|
188
|
+
report.emitted++;
|
|
189
|
+
}
|
|
190
|
+
while (decision.length) {
|
|
191
|
+
const item = decision.shift();
|
|
192
|
+
emitFn(await decide(item));
|
|
193
|
+
counters.emitted++;
|
|
194
|
+
report.processed++;
|
|
195
|
+
report.emitted++;
|
|
196
|
+
}
|
|
197
|
+
const t = now();
|
|
198
|
+
for (const [key, entry] of progressPending) {
|
|
199
|
+
if (t - entry.firstArrival < coalesceWindowMs) {
|
|
200
|
+
counters.deferred++;
|
|
201
|
+
report.deferred++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
progressPending.delete(key);
|
|
205
|
+
emitFn(await decide(entry.item));
|
|
206
|
+
counters.emitted++;
|
|
207
|
+
report.processed++;
|
|
208
|
+
report.emitted++;
|
|
209
|
+
}
|
|
210
|
+
return report;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function stats() {
|
|
214
|
+
return {
|
|
215
|
+
...counters,
|
|
216
|
+
pending: critical.length + decision.length + progressPending.size,
|
|
217
|
+
closedWith,
|
|
218
|
+
closed,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function close() {
|
|
223
|
+
if (closed) return;
|
|
224
|
+
closed = true;
|
|
225
|
+
closedWith = critical.length + decision.length + progressPending.size;
|
|
226
|
+
critical.length = 0;
|
|
227
|
+
decision.length = 0;
|
|
228
|
+
progressPending.clear();
|
|
229
|
+
for (const h of timers) clearTimeout(h);
|
|
230
|
+
timers.clear();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Bounded frozen receipts of shadow-stage resource-policy consults (G6). */
|
|
234
|
+
function resourcePolicyReceipts() {
|
|
235
|
+
return [...resourceReceipts];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { submit, flush, stats, close, resourcePolicyReceipts };
|
|
239
|
+
}
|
package/src/index/buildIndex.js
CHANGED
|
@@ -255,9 +255,9 @@ export async function buildCodeIndex({ rootDir = process.cwd(), changedFiles = n
|
|
|
255
255
|
&& Array.isArray(previousCallsArtifact.items);
|
|
256
256
|
|
|
257
257
|
if (canReuseParsedArtifacts) {
|
|
258
|
-
const previousSymbolsByPath = groupBy(previousSymbolsArtifact.items ?? [], (item) => item
|
|
259
|
-
const previousImportsByPath = groupBy(previousImportsArtifact.items ?? [], (item) => item
|
|
260
|
-
const previousCallsByPath = groupBy(previousCallsArtifact.items ?? [], (item) => item
|
|
258
|
+
const previousSymbolsByPath = groupBy(previousSymbolsArtifact.items ?? [], (item) => item?.filePath);
|
|
259
|
+
const previousImportsByPath = groupBy(previousImportsArtifact.items ?? [], (item) => item?.from);
|
|
260
|
+
const previousCallsByPath = groupBy(previousCallsArtifact.items ?? [], (item) => item?.filePath);
|
|
261
261
|
|
|
262
262
|
for (const filePath of reusableCodeFiles) {
|
|
263
263
|
symbols.push(...(previousSymbolsByPath.get(filePath) ?? []));
|
|
@@ -891,9 +891,9 @@ async function tryIncrementalIndexUpdate({ absoluteRoot, indexDir, changedPaths,
|
|
|
891
891
|
}
|
|
892
892
|
const styleFilePaths = styleEntries.filter(Boolean).map((entry) => normalizeRelative(absoluteRoot, entry.absolutePath));
|
|
893
893
|
|
|
894
|
-
const previousSymbolsByPath = groupBy(symbolsArtifact.items ?? [], (item) => item
|
|
895
|
-
const previousImportsByPath = groupBy(importsArtifact.items ?? [], (item) => item
|
|
896
|
-
const previousCallsByPath = groupBy(callsArtifact.items ?? [], (item) => item
|
|
894
|
+
const previousSymbolsByPath = groupBy(symbolsArtifact.items ?? [], (item) => item?.filePath);
|
|
895
|
+
const previousImportsByPath = groupBy(importsArtifact.items ?? [], (item) => item?.from);
|
|
896
|
+
const previousCallsByPath = groupBy(callsArtifact.items ?? [], (item) => item?.filePath);
|
|
897
897
|
|
|
898
898
|
const parsedSet = new Set(parsedFiles);
|
|
899
899
|
const codeFiles = mergedFileRecords.filter((record) => CODE_EXTENSIONS.has(record.ext));
|
|
@@ -2385,8 +2385,8 @@ const ARCHETYPE_RULES = [
|
|
|
2385
2385
|
{ pattern: /(?:^|\/)(?:models?|schemas?)\//, archetype: 'schema' },
|
|
2386
2386
|
];
|
|
2387
2387
|
|
|
2388
|
-
function classifyArchetypes(fileRecords, symbols) {
|
|
2389
|
-
const symbolNamesByPath = groupBy(symbols, (s) => s
|
|
2388
|
+
export function classifyArchetypes(fileRecords, symbols) {
|
|
2389
|
+
const symbolNamesByPath = groupBy(symbols, (s) => s?.filePath);
|
|
2390
2390
|
|
|
2391
2391
|
return fileRecords
|
|
2392
2392
|
.filter((f) => CODE_EXTENSIONS.has(f.ext))
|
|
@@ -2409,12 +2409,13 @@ function classifyArchetypes(fileRecords, symbols) {
|
|
|
2409
2409
|
|
|
2410
2410
|
if (!archetype) {
|
|
2411
2411
|
const fileSymbols = symbolNamesByPath.get(file.filePath) ?? [];
|
|
2412
|
-
const hasDefaultExport = fileSymbols.some((s) => s
|
|
2412
|
+
const hasDefaultExport = fileSymbols.some((s) => s?.name === 'default');
|
|
2413
2413
|
const hasComponentSignal = file.ext === '.vue'
|
|
2414
2414
|
|| fileSymbols.some((s) =>
|
|
2415
|
-
s
|
|
2416
|
-
|| s
|
|
2417
|
-
|
|
2415
|
+
s?.type === 'component-name'
|
|
2416
|
+
|| (typeof s?.name === 'string'
|
|
2417
|
+
&& (s.name.toLowerCase().includes('component')
|
|
2418
|
+
|| s.name.toLowerCase().includes('page'))),
|
|
2418
2419
|
);
|
|
2419
2420
|
|
|
2420
2421
|
if ((hasDefaultExport || file.ext === '.vue') && hasComponentSignal) {
|