martin-loop 0.1.4 → 1.3.0
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/CODE_OF_CONDUCT.md +32 -0
- package/README.md +172 -227
- package/demo/seeded-workspace/README.md +35 -0
- package/demo/seeded-workspace/TASKS.md +29 -0
- package/demo/seeded-workspace/martin.config.yaml +11 -0
- package/demo/seeded-workspace/package.json +8 -0
- package/demo/seeded-workspace/src/invoice-summary.js +11 -0
- package/demo/seeded-workspace/test/invoice-summary.test.js +20 -0
- package/dist/bin/martin-loop.js +0 -0
- package/dist/vendor/adapters/claude-cli.d.ts +19 -4
- package/dist/vendor/adapters/claude-cli.js +55 -24
- package/dist/vendor/adapters/cli-bridge.d.ts +1 -0
- package/dist/vendor/adapters/cli-bridge.js +154 -28
- package/dist/vendor/adapters/counter.d.ts +1 -0
- package/dist/vendor/adapters/counter.js +4 -0
- package/dist/vendor/adapters/git-baseline.d.ts +50 -0
- package/dist/vendor/adapters/git-baseline.js +233 -0
- package/dist/vendor/adapters/index.d.ts +1 -0
- package/dist/vendor/adapters/index.js +1 -0
- package/dist/vendor/adapters/openrouter-adapter.d.ts +15 -0
- package/dist/vendor/adapters/openrouter-adapter.js +302 -0
- package/dist/vendor/adapters/usage.d.ts +48 -0
- package/dist/vendor/adapters/usage.js +66 -0
- package/dist/vendor/adapters/verifier-only.d.ts +7 -0
- package/dist/vendor/adapters/verifier-only.js +57 -0
- package/dist/vendor/cli/bin/exit.d.ts +12 -0
- package/dist/vendor/cli/bin/exit.js +28 -0
- package/dist/vendor/cli/commands/analyze.d.ts +5 -0
- package/dist/vendor/cli/commands/analyze.js +58 -0
- package/dist/vendor/cli/commands/audit-log-verify.d.ts +34 -0
- package/dist/vendor/cli/commands/audit-log-verify.js +99 -0
- package/dist/vendor/cli/commands/audit.d.ts +8 -0
- package/dist/vendor/cli/commands/audit.js +199 -0
- package/dist/vendor/cli/commands/corpus.d.ts +5 -0
- package/dist/vendor/cli/commands/corpus.js +60 -0
- package/dist/vendor/cli/commands/doctor.d.ts +8 -0
- package/dist/vendor/cli/commands/doctor.js +219 -0
- package/dist/vendor/cli/commands/explain.d.ts +17 -0
- package/dist/vendor/cli/commands/explain.js +176 -0
- package/dist/vendor/cli/commands/export.d.ts +5 -0
- package/dist/vendor/cli/commands/export.js +60 -0
- package/dist/vendor/cli/commands/governance.d.ts +8 -0
- package/dist/vendor/cli/commands/governance.js +95 -0
- package/dist/vendor/cli/commands/improve.d.ts +18 -0
- package/dist/vendor/cli/commands/improve.js +396 -0
- package/dist/vendor/cli/commands/init.d.ts +8 -0
- package/dist/vendor/cli/commands/init.js +281 -0
- package/dist/vendor/cli/commands/migration.d.ts +8 -0
- package/dist/vendor/cli/commands/migration.js +67 -0
- package/dist/vendor/cli/commands/prior.d.ts +23 -0
- package/dist/vendor/cli/commands/prior.js +145 -0
- package/dist/vendor/cli/commands/resume.d.ts +21 -0
- package/dist/vendor/cli/commands/resume.js +73 -0
- package/dist/vendor/cli/commands/verify.d.ts +6 -0
- package/dist/vendor/cli/commands/verify.js +43 -0
- package/dist/vendor/cli/index.d.ts +6 -1
- package/dist/vendor/cli/index.js +124 -7
- package/dist/vendor/cli/research/public-corpus.d.ts +43 -0
- package/dist/vendor/cli/research/public-corpus.js +151 -0
- package/dist/vendor/cli/ui/error-card.d.ts +38 -0
- package/dist/vendor/cli/ui/error-card.js +103 -0
- package/dist/vendor/cli/ui/mission-brief.d.ts +41 -0
- package/dist/vendor/cli/ui/mission-brief.js +173 -0
- package/dist/vendor/cli/ui/summary-card.d.ts +34 -0
- package/dist/vendor/cli/ui/summary-card.js +102 -0
- package/dist/vendor/contracts/audit.d.ts +46 -0
- package/dist/vendor/contracts/audit.js +360 -0
- package/dist/vendor/contracts/index.d.ts +3 -1
- package/dist/vendor/contracts/post-phase15.d.ts +240 -0
- package/dist/vendor/contracts/post-phase15.js +166 -0
- package/dist/vendor/core/agent/mandates.d.ts +46 -0
- package/dist/vendor/core/agent/mandates.js +178 -0
- package/dist/vendor/core/agent/receipts.d.ts +38 -0
- package/dist/vendor/core/agent/receipts.js +131 -0
- package/dist/vendor/core/agent/signing.d.ts +17 -0
- package/dist/vendor/core/agent/signing.js +91 -0
- package/dist/vendor/core/attestation/sign.d.ts +25 -0
- package/dist/vendor/core/attestation/sign.js +216 -0
- package/dist/vendor/core/autonomy/autonomous-promotion.d.ts +120 -0
- package/dist/vendor/core/autonomy/autonomous-promotion.js +346 -0
- package/dist/vendor/core/autonomy/envelope-v2.d.ts +29 -0
- package/dist/vendor/core/autonomy/envelope-v2.js +60 -0
- package/dist/vendor/core/autonomy/envelope.d.ts +17 -0
- package/dist/vendor/core/autonomy/envelope.js +27 -0
- package/dist/vendor/core/autonomy/escalation-ledger.d.ts +20 -0
- package/dist/vendor/core/autonomy/escalation-ledger.js +18 -0
- package/dist/vendor/core/autonomy/resume.d.ts +15 -0
- package/dist/vendor/core/autonomy/resume.js +23 -0
- package/dist/vendor/core/circuit/circuit-breaker.d.ts +60 -0
- package/dist/vendor/core/circuit/circuit-breaker.js +143 -0
- package/dist/vendor/core/compiler.d.ts +2 -0
- package/dist/vendor/core/compiler.js +10 -4
- package/dist/vendor/core/context-distillation.d.ts +3 -0
- package/dist/vendor/core/context-distillation.js +44 -0
- package/dist/vendor/core/context-flow/compile-context.d.ts +8 -0
- package/dist/vendor/core/context-flow/compile-context.js +111 -0
- package/dist/vendor/core/context-flow/entities.d.ts +2 -0
- package/dist/vendor/core/context-flow/entities.js +44 -0
- package/dist/vendor/core/context-flow/evaluate-policy.d.ts +2 -0
- package/dist/vendor/core/context-flow/evaluate-policy.js +42 -0
- package/dist/vendor/core/context-flow/index.d.ts +11 -0
- package/dist/vendor/core/context-flow/index.js +24 -0
- package/dist/vendor/core/context-flow/labels.d.ts +3 -0
- package/dist/vendor/core/context-flow/labels.js +17 -0
- package/dist/vendor/core/context-flow/normalizer.d.ts +9 -0
- package/dist/vendor/core/context-flow/normalizer.js +69 -0
- package/dist/vendor/core/context-flow/profiles.d.ts +33 -0
- package/dist/vendor/core/context-flow/profiles.js +36 -0
- package/dist/vendor/core/context-flow/redaction.d.ts +1 -0
- package/dist/vendor/core/context-flow/redaction.js +6 -0
- package/dist/vendor/core/context-flow/sensitivity.d.ts +2 -0
- package/dist/vendor/core/context-flow/sensitivity.js +27 -0
- package/dist/vendor/core/context-flow/sync-preview.d.ts +2 -0
- package/dist/vendor/core/context-flow/sync-preview.js +22 -0
- package/dist/vendor/core/context-flow/token-estimator.d.ts +3 -0
- package/dist/vendor/core/context-flow/token-estimator.js +13 -0
- package/dist/vendor/core/context-flow/types.d.ts +91 -0
- package/dist/vendor/core/context-flow/types.js +2 -0
- package/dist/vendor/core/context-integrity.d.ts +26 -0
- package/dist/vendor/core/context-integrity.js +56 -0
- package/dist/vendor/core/context-utility.d.ts +47 -0
- package/dist/vendor/core/context-utility.js +405 -0
- package/dist/vendor/core/cost/pipeline.d.ts +92 -0
- package/dist/vendor/core/cost/pipeline.js +141 -0
- package/dist/vendor/core/cost/tagged-cost.d.ts +27 -0
- package/dist/vendor/core/cost/tagged-cost.js +55 -0
- package/dist/vendor/core/cost-governor.d.ts +2 -0
- package/dist/vendor/core/cost-governor.js +50 -0
- package/dist/vendor/core/cve/cve-check.d.ts +80 -0
- package/dist/vendor/core/cve/cve-check.js +172 -0
- package/dist/vendor/core/digital-twin/index.d.ts +27 -0
- package/dist/vendor/core/digital-twin/index.js +90 -0
- package/dist/vendor/core/drift/drift-graph.d.ts +47 -0
- package/dist/vendor/core/drift/drift-graph.js +100 -0
- package/dist/vendor/core/drift/objective-lock.d.ts +69 -0
- package/dist/vendor/core/drift/objective-lock.js +88 -0
- package/dist/vendor/core/drift/scope.d.ts +46 -0
- package/dist/vendor/core/drift/scope.js +102 -0
- package/dist/vendor/core/drift/signature-lock.d.ts +48 -0
- package/dist/vendor/core/drift/signature-lock.js +202 -0
- package/dist/vendor/core/drift/stale-proof-gate.d.ts +21 -0
- package/dist/vendor/core/drift/stale-proof-gate.js +19 -0
- package/dist/vendor/core/eval/known-bad-world-runner.d.ts +24 -0
- package/dist/vendor/core/eval/known-bad-world-runner.js +256 -0
- package/dist/vendor/core/evidence/claim-audit.d.ts +18 -0
- package/dist/vendor/core/evidence/claim-audit.js +89 -0
- package/dist/vendor/core/exit-intelligence.d.ts +2 -0
- package/dist/vendor/core/exit-intelligence.js +58 -0
- package/dist/vendor/core/explain/formatter.d.ts +42 -0
- package/dist/vendor/core/explain/formatter.js +171 -0
- package/dist/vendor/core/explain/timeline.d.ts +29 -0
- package/dist/vendor/core/explain/timeline.js +213 -0
- package/dist/vendor/core/failure-taxonomy.d.ts +2 -0
- package/dist/vendor/core/failure-taxonomy.js +76 -0
- package/dist/vendor/core/gateway/index.d.ts +10 -0
- package/dist/vendor/core/gateway/index.js +12 -0
- package/dist/vendor/core/gateway/registry.d.ts +40 -0
- package/dist/vendor/core/gateway/registry.js +97 -0
- package/dist/vendor/core/gateway/transport.d.ts +31 -0
- package/dist/vendor/core/gateway/transport.js +82 -0
- package/dist/vendor/core/gateway/vault.d.ts +19 -0
- package/dist/vendor/core/gateway/vault.js +29 -0
- package/dist/vendor/core/graph/adapters.d.ts +43 -0
- package/dist/vendor/core/graph/adapters.js +91 -0
- package/dist/vendor/core/graph/hotspots.d.ts +22 -0
- package/dist/vendor/core/graph/hotspots.js +30 -0
- package/dist/vendor/core/graph/index.d.ts +1 -0
- package/dist/vendor/core/graph/index.js +2 -0
- package/dist/vendor/core/honey/honey-tokens.d.ts +32 -0
- package/dist/vendor/core/honey/honey-tokens.js +44 -0
- package/dist/vendor/core/index.d.ts +7 -4
- package/dist/vendor/core/index.js +222 -64
- package/dist/vendor/core/learning/bayesian-update.d.ts +31 -0
- package/dist/vendor/core/learning/bayesian-update.js +60 -0
- package/dist/vendor/core/learning/prior-sets.d.ts +42 -0
- package/dist/vendor/core/learning/prior-sets.js +111 -0
- package/dist/vendor/core/learning/promotion-gate.d.ts +17 -0
- package/dist/vendor/core/learning/promotion-gate.js +23 -0
- package/dist/vendor/core/leash/blast-radius.d.ts +42 -0
- package/dist/vendor/core/leash/blast-radius.js +156 -0
- package/dist/vendor/core/leash/policy-leash.d.ts +31 -0
- package/dist/vendor/core/leash/policy-leash.js +117 -0
- package/dist/vendor/core/memo/memo.d.ts +63 -0
- package/dist/vendor/core/memo/memo.js +97 -0
- package/dist/vendor/core/memory/learning-pipeline.d.ts +154 -0
- package/dist/vendor/core/memory/learning-pipeline.js +391 -0
- package/dist/vendor/core/memory/palace.d.ts +84 -0
- package/dist/vendor/core/memory/palace.js +379 -0
- package/dist/vendor/core/merge/ast-merge.d.ts +22 -0
- package/dist/vendor/core/merge/ast-merge.js +350 -0
- package/dist/vendor/core/merge/text-merge.d.ts +12 -0
- package/dist/vendor/core/merge/text-merge.js +182 -0
- package/dist/vendor/core/otel/tracer.d.ts +45 -0
- package/dist/vendor/core/otel/tracer.js +116 -0
- package/dist/vendor/core/parallel/parallel-attempts.d.ts +28 -0
- package/dist/vendor/core/parallel/parallel-attempts.js +41 -0
- package/dist/vendor/core/parallel/scorer.d.ts +24 -0
- package/dist/vendor/core/parallel/scorer.js +65 -0
- package/dist/vendor/core/pattern-detection.d.ts +64 -0
- package/dist/vendor/core/pattern-detection.js +108 -0
- package/dist/vendor/core/persistence/checkpoint.d.ts +44 -0
- package/dist/vendor/core/persistence/checkpoint.js +156 -0
- package/dist/vendor/core/persistence/cleanup.d.ts +22 -0
- package/dist/vendor/core/persistence/cleanup.js +131 -0
- package/dist/vendor/core/persistence/index.d.ts +2 -0
- package/dist/vendor/core/persistence/index.js +1 -0
- package/dist/vendor/core/persistence/runs-reader.d.ts +52 -0
- package/dist/vendor/core/persistence/runs-reader.js +84 -0
- package/dist/vendor/core/persistence/store.d.ts +6 -1
- package/dist/vendor/core/persistence/store.js +5 -0
- package/dist/vendor/core/policy/file-touch-quota.d.ts +60 -0
- package/dist/vendor/core/policy/file-touch-quota.js +105 -0
- package/dist/vendor/core/policy/policy-loader.d.ts +30 -0
- package/dist/vendor/core/policy/policy-loader.js +170 -0
- package/dist/vendor/core/policy/policy-schema.d.ts +55 -0
- package/dist/vendor/core/policy/policy-schema.js +78 -0
- package/dist/vendor/core/policy.d.ts +6 -0
- package/dist/vendor/core/probe/probe.d.ts +49 -0
- package/dist/vendor/core/probe/probe.js +115 -0
- package/dist/vendor/core/proof/patch-proof.d.ts +58 -0
- package/dist/vendor/core/proof/patch-proof.js +84 -0
- package/dist/vendor/core/proof/semantic-probe.d.ts +25 -0
- package/dist/vendor/core/proof/semantic-probe.js +82 -0
- package/dist/vendor/core/recovery/failure-mode-runner.d.ts +29 -0
- package/dist/vendor/core/recovery/failure-mode-runner.js +39 -0
- package/dist/vendor/core/red-blue/red-phase.d.ts +64 -0
- package/dist/vendor/core/red-blue/red-phase.js +141 -0
- package/dist/vendor/core/red-blue/risk-tiers.d.ts +22 -0
- package/dist/vendor/core/red-blue/risk-tiers.js +33 -0
- package/dist/vendor/core/replay/replay.d.ts +85 -0
- package/dist/vendor/core/replay/replay.js +109 -0
- package/dist/vendor/core/router/engine.d.ts +54 -0
- package/dist/vendor/core/router/engine.js +131 -0
- package/dist/vendor/core/router/index.d.ts +1 -0
- package/dist/vendor/core/router/index.js +2 -0
- package/dist/vendor/core/router/trust-calibration.d.ts +57 -0
- package/dist/vendor/core/router/trust-calibration.js +127 -0
- package/dist/vendor/core/run-martin.d.ts +2 -0
- package/dist/vendor/core/run-martin.js +287 -0
- package/dist/vendor/core/security/cve-scanner.d.ts +62 -0
- package/dist/vendor/core/security/cve-scanner.js +178 -0
- package/dist/vendor/core/sentinel/efficiency-sentinel.d.ts +29 -0
- package/dist/vendor/core/sentinel/efficiency-sentinel.js +30 -0
- package/dist/vendor/core/sentinel/progress-guard.d.ts +35 -0
- package/dist/vendor/core/sentinel/progress-guard.js +46 -0
- package/dist/vendor/core/siem/siem-emitter.d.ts +49 -0
- package/dist/vendor/core/siem/siem-emitter.js +157 -0
- package/dist/vendor/core/strategy/attempt-brief.d.ts +22 -0
- package/dist/vendor/core/strategy/attempt-brief.js +89 -0
- package/dist/vendor/core/summarize/diff-summary.d.ts +35 -0
- package/dist/vendor/core/summarize/diff-summary.js +204 -0
- package/dist/vendor/core/surface-signals.d.ts +21 -0
- package/dist/vendor/core/surface-signals.js +139 -0
- package/dist/vendor/core/truth/truth-wall.d.ts +51 -0
- package/dist/vendor/core/truth/truth-wall.js +69 -0
- package/dist/vendor/core/truth-spine.d.ts +26 -0
- package/dist/vendor/core/truth-spine.js +62 -0
- package/dist/vendor/core/types.d.ts +115 -0
- package/dist/vendor/core/types.js +2 -0
- package/dist/vendor/core/verification/tiered-verify.d.ts +17 -0
- package/dist/vendor/core/verification/tiered-verify.js +29 -0
- package/dist/vendor/core/verifier-pyramid.d.ts +32 -0
- package/dist/vendor/core/verifier-pyramid.js +111 -0
- package/dist/vendor/core/workflow-artifacts.d.ts +99 -0
- package/dist/vendor/core/workflow-artifacts.js +668 -0
- package/dist/vendor/core/wrap/supervised-run.d.ts +96 -0
- package/dist/vendor/core/wrap/supervised-run.js +178 -0
- package/docs/assets/cli-animated.svg +139 -0
- package/docs/assets/cli-static.svg +34 -0
- package/docs/assets/github-hero-v2.svg +23 -0
- package/docs/assets/martin-raplph.png.jpg +0 -0
- package/docs/assets/martinloop-logo.png +0 -0
- package/docs/assets/nvidia-inception-program-light.png +0 -0
- package/docs/assets/nvidia-inception-program.png +0 -0
- package/docs/assets/phase3c-sidesidebyside-demo.html +228 -0
- package/docs/assets/side-by-side.svg +134 -0
- package/docs/oss/CLAUDE-CODE-WALKTHROUGH.md +142 -0
- package/docs/oss/EXAMPLES.md +9 -1
- package/docs/oss/OSS-BOUNDARY-REPORT.json +109 -113
- package/docs/oss/OSS-BOUNDARY-REPORT.md +48 -48
- package/docs/oss/QUICKSTART.md +39 -4
- package/docs/oss/RALPH-LOOP-SAFETY.md +113 -0
- package/docs/oss/README.md +7 -4
- package/docs/oss/RELEASE-SURFACE-REPORT.json +46 -45
- package/docs/oss/RELEASE-SURFACE-REPORT.md +36 -35
- package/package.json +129 -49
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
const CONTRACT_WEIGHT = 0.22;
|
|
2
|
+
const GUIDANCE_WEIGHT = 0.18;
|
|
3
|
+
const MEMORY_WEIGHT = 0.16;
|
|
4
|
+
const VERIFICATION_WEIGHT = 0.20;
|
|
5
|
+
const ANOMALY_WEIGHT = 0.14;
|
|
6
|
+
const SURFACE_WEIGHT = 0.10;
|
|
7
|
+
export function computeContextUtilityScore(input) {
|
|
8
|
+
const compiledContext = normalizePromptPacket(input.compiledContext);
|
|
9
|
+
const verification = resolveVerificationCounts(compiledContext, input.verificationPlan);
|
|
10
|
+
const anomalies = resolvePatterns(input.patterns);
|
|
11
|
+
const scopeSurface = resolveScopeSurface(input.scopeSurface);
|
|
12
|
+
const memoryEntries = compiledContext.memoryBrief?.entries ?? [];
|
|
13
|
+
const memoryDecisions = scoreMemoryEntries(compiledContext, memoryEntries);
|
|
14
|
+
const retainedMemory = memoryDecisions.filter((decision) => decision.utility >= 55);
|
|
15
|
+
const evictableMemory = memoryDecisions.filter((decision) => decision.utility < 40);
|
|
16
|
+
const sectionScores = {
|
|
17
|
+
contract: scoreContract(compiledContext),
|
|
18
|
+
guidance: scoreGuidance(compiledContext),
|
|
19
|
+
memory: scoreMemory(memoryDecisions),
|
|
20
|
+
verification: scoreVerification(verification),
|
|
21
|
+
anomalyGuards: scoreAnomalyGuards(anomalies.length),
|
|
22
|
+
surfaceFocus: scoreSurfaceFocus(scopeSurface.touchedSurfaces.length)
|
|
23
|
+
};
|
|
24
|
+
const utilityScore = Math.round(sectionScores.contract * CONTRACT_WEIGHT +
|
|
25
|
+
sectionScores.guidance * GUIDANCE_WEIGHT +
|
|
26
|
+
sectionScores.memory * MEMORY_WEIGHT +
|
|
27
|
+
sectionScores.verification * VERIFICATION_WEIGHT +
|
|
28
|
+
sectionScores.anomalyGuards * ANOMALY_WEIGHT +
|
|
29
|
+
sectionScores.surfaceFocus * SURFACE_WEIGHT);
|
|
30
|
+
const utilityBand = classifyUtilityBand(utilityScore);
|
|
31
|
+
const evidenceCounts = {
|
|
32
|
+
memoryEntryCount: memoryEntries.length,
|
|
33
|
+
retainedMemoryCount: retainedMemory.length,
|
|
34
|
+
evictableMemoryCount: evictableMemory.length,
|
|
35
|
+
priorFailurePatternCount: compiledContext.priorFailurePatterns.length,
|
|
36
|
+
anomalyCount: anomalies.length,
|
|
37
|
+
requiredCheckCount: verification.requiredCheckCount,
|
|
38
|
+
optionalCheckCount: verification.optionalCheckCount,
|
|
39
|
+
touchedSurfaceCount: scopeSurface.touchedSurfaces.length,
|
|
40
|
+
changedFileCount: scopeSurface.changedFileCount,
|
|
41
|
+
acceptanceCriteriaCount: compiledContext.contract.acceptanceCriteria?.length ?? 0
|
|
42
|
+
};
|
|
43
|
+
const actions = buildContextUtilityActions({
|
|
44
|
+
utilityScore,
|
|
45
|
+
utilityBand,
|
|
46
|
+
verification,
|
|
47
|
+
anomalyCount: anomalies.length,
|
|
48
|
+
touchedSurfaceCount: scopeSurface.touchedSurfaces.length,
|
|
49
|
+
retainedMemory,
|
|
50
|
+
evictableMemory
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
schemaVersion: "martin.context-utility-score.v1",
|
|
54
|
+
attemptIndex: input.attemptIndex,
|
|
55
|
+
utilityScore,
|
|
56
|
+
utilityBand,
|
|
57
|
+
summary: buildSummary({
|
|
58
|
+
utilityScore,
|
|
59
|
+
utilityBand,
|
|
60
|
+
anomalyCount: anomalies.length,
|
|
61
|
+
evictableMemoryCount: evictableMemory.length,
|
|
62
|
+
shouldCompress: actions.some((action) => action.type === "compress_context")
|
|
63
|
+
}),
|
|
64
|
+
sectionScores,
|
|
65
|
+
actions,
|
|
66
|
+
evidenceCounts
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function classifyUtilityBand(score) {
|
|
70
|
+
if (score >= 75) {
|
|
71
|
+
return "high";
|
|
72
|
+
}
|
|
73
|
+
if (score >= 50) {
|
|
74
|
+
return "medium";
|
|
75
|
+
}
|
|
76
|
+
return "low";
|
|
77
|
+
}
|
|
78
|
+
function normalizePromptPacket(compiledContext) {
|
|
79
|
+
const packet = isRecord(compiledContext) ? compiledContext : {};
|
|
80
|
+
const contract = isRecord(packet["contract"]) ? packet["contract"] : {};
|
|
81
|
+
const memoryBrief = isRecord(packet["memoryBrief"])
|
|
82
|
+
? packet["memoryBrief"]
|
|
83
|
+
: undefined;
|
|
84
|
+
const budgetEnvelope = isRecord(packet["budgetEnvelope"])
|
|
85
|
+
? packet["budgetEnvelope"]
|
|
86
|
+
: {};
|
|
87
|
+
const allowedPaths = readStringArray(contract["allowedPaths"]);
|
|
88
|
+
const deniedPaths = readStringArray(contract["deniedPaths"]);
|
|
89
|
+
const acceptanceCriteria = readStringArray(contract["acceptanceCriteria"]);
|
|
90
|
+
const entries = normalizeMemoryRecallEntries(memoryBrief?.["entries"]);
|
|
91
|
+
return {
|
|
92
|
+
loopId: readString(packet["loopId"]) ?? "unknown-loop",
|
|
93
|
+
attemptNumber: readFiniteNumber(packet["attemptNumber"]),
|
|
94
|
+
contract: {
|
|
95
|
+
objective: readString(contract["objective"]) ?? "",
|
|
96
|
+
verificationPlan: readStringArray(contract["verificationPlan"]),
|
|
97
|
+
...(allowedPaths.length > 0 ? { allowedPaths } : {}),
|
|
98
|
+
...(deniedPaths.length > 0 ? { deniedPaths } : {}),
|
|
99
|
+
...(acceptanceCriteria.length > 0 ? { acceptanceCriteria } : {})
|
|
100
|
+
},
|
|
101
|
+
priorFailurePatterns: readStringArray(packet["priorFailurePatterns"]),
|
|
102
|
+
...(memoryBrief
|
|
103
|
+
? {
|
|
104
|
+
memoryBrief: {
|
|
105
|
+
wingKey: readString(memoryBrief["wingKey"]) ?? "",
|
|
106
|
+
recalledAt: readString(memoryBrief["recalledAt"]) ?? "",
|
|
107
|
+
entries
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
: {}),
|
|
111
|
+
guidance: readString(packet["guidance"]) ?? "",
|
|
112
|
+
budgetEnvelope: {
|
|
113
|
+
remainingBudgetUsd: readFiniteNumber(budgetEnvelope["remainingBudgetUsd"]),
|
|
114
|
+
remainingIterations: readFiniteNumber(budgetEnvelope["remainingIterations"]),
|
|
115
|
+
remainingTokens: readFiniteNumber(budgetEnvelope["remainingTokens"])
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function normalizeMemoryRecallEntries(value) {
|
|
120
|
+
if (!Array.isArray(value)) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
return value.map((entry, index) => {
|
|
124
|
+
const normalizedEntry = isRecord(entry) ? entry : {};
|
|
125
|
+
const sourceAttemptIndex = normalizedEntry["sourceAttemptIndex"];
|
|
126
|
+
return {
|
|
127
|
+
drawerId: readString(normalizedEntry["drawerId"]) ?? `memory-${String(index + 1)}`,
|
|
128
|
+
roomKey: readString(normalizedEntry["roomKey"]) ?? "",
|
|
129
|
+
lesson: readString(normalizedEntry["lesson"]) ?? "",
|
|
130
|
+
content: readString(normalizedEntry["content"]) ?? "",
|
|
131
|
+
sourceRunId: readString(normalizedEntry["sourceRunId"]) ?? "",
|
|
132
|
+
...(typeof sourceAttemptIndex === "number" && Number.isFinite(sourceAttemptIndex)
|
|
133
|
+
? { sourceAttemptIndex }
|
|
134
|
+
: {}),
|
|
135
|
+
score: readFiniteNumber(normalizedEntry["score"]),
|
|
136
|
+
reason: readString(normalizedEntry["reason"]) ?? ""
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function scoreContract(compiledContext) {
|
|
141
|
+
const boundaryCount = [
|
|
142
|
+
compiledContext.contract.allowedPaths?.length ? 1 : 0,
|
|
143
|
+
compiledContext.contract.deniedPaths?.length ? 1 : 0,
|
|
144
|
+
compiledContext.contract.acceptanceCriteria?.length ? 1 : 0
|
|
145
|
+
].reduce((sum, value) => sum + value, 0);
|
|
146
|
+
return clampScore((compiledContext.contract.objective.trim().length > 0 ? 40 : 0) +
|
|
147
|
+
Math.min(compiledContext.contract.verificationPlan.length, 3) * 15 +
|
|
148
|
+
boundaryCount * 5);
|
|
149
|
+
}
|
|
150
|
+
function scoreGuidance(compiledContext) {
|
|
151
|
+
const guidance = compiledContext.guidance.toLowerCase();
|
|
152
|
+
let score = guidance.trim().length > 0 ? 25 : 0;
|
|
153
|
+
if (guidance.includes("allowed paths")) {
|
|
154
|
+
score += 15;
|
|
155
|
+
}
|
|
156
|
+
if (guidance.includes("denied paths")) {
|
|
157
|
+
score += 10;
|
|
158
|
+
}
|
|
159
|
+
if (guidance.includes("prior failure patterns")) {
|
|
160
|
+
score += 10;
|
|
161
|
+
}
|
|
162
|
+
if (guidance.includes("current focus")) {
|
|
163
|
+
score += 15;
|
|
164
|
+
}
|
|
165
|
+
if (compiledContext.guidance.length >= 180) {
|
|
166
|
+
score += 15;
|
|
167
|
+
}
|
|
168
|
+
return clampScore(score);
|
|
169
|
+
}
|
|
170
|
+
function scoreMemory(memoryDecisions) {
|
|
171
|
+
if (memoryDecisions.length === 0) {
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
const total = memoryDecisions.reduce((sum, decision) => sum + decision.utility, 0);
|
|
175
|
+
return clampScore(Math.round(total / memoryDecisions.length));
|
|
176
|
+
}
|
|
177
|
+
function scoreVerification(counts) {
|
|
178
|
+
return clampScore(counts.requiredCheckCount * 25 +
|
|
179
|
+
counts.optionalCheckCount * 8 +
|
|
180
|
+
(counts.mustPassAll ? 9 : 0) +
|
|
181
|
+
(counts.hasTieredPlan ? 8 : 0));
|
|
182
|
+
}
|
|
183
|
+
function scoreAnomalyGuards(anomalyCount) {
|
|
184
|
+
return clampScore(100 - anomalyCount * 30);
|
|
185
|
+
}
|
|
186
|
+
function scoreSurfaceFocus(touchedSurfaceCount) {
|
|
187
|
+
if (touchedSurfaceCount === 0) {
|
|
188
|
+
return 40;
|
|
189
|
+
}
|
|
190
|
+
if (touchedSurfaceCount <= 4) {
|
|
191
|
+
return 100;
|
|
192
|
+
}
|
|
193
|
+
if (touchedSurfaceCount <= 8) {
|
|
194
|
+
return 70;
|
|
195
|
+
}
|
|
196
|
+
if (touchedSurfaceCount <= 12) {
|
|
197
|
+
return 45;
|
|
198
|
+
}
|
|
199
|
+
return 20;
|
|
200
|
+
}
|
|
201
|
+
function buildContextUtilityActions(input) {
|
|
202
|
+
const actions = [];
|
|
203
|
+
if (input.retainedMemory.length > 0) {
|
|
204
|
+
actions.push({
|
|
205
|
+
type: "retain_memory",
|
|
206
|
+
priority: "medium",
|
|
207
|
+
message: "Retain only the highest-signal memory drawers in the active brief.",
|
|
208
|
+
drawerIds: input.retainedMemory.map((decision) => decision.entry.drawerId)
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (input.evictableMemory.length > 0) {
|
|
212
|
+
actions.push({
|
|
213
|
+
type: "evict_memory",
|
|
214
|
+
priority: input.utilityBand === "low" ? "high" : "medium",
|
|
215
|
+
message: "Evict low-value memory drawers from the active context before the next retry.",
|
|
216
|
+
drawerIds: input.evictableMemory.map((decision) => decision.entry.drawerId)
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
if (input.utilityBand === "low" ||
|
|
220
|
+
input.anomalyCount >= 3 ||
|
|
221
|
+
input.touchedSurfaceCount >= 9) {
|
|
222
|
+
actions.push({
|
|
223
|
+
type: "compress_context",
|
|
224
|
+
priority: "high",
|
|
225
|
+
message: "Compress the active context to the contract, retained memory drawers, latest verifier evidence, and touched surfaces only."
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (input.verification.requiredCheckCount === 0 || input.verification.optionalCheckCount === 0) {
|
|
229
|
+
actions.push({
|
|
230
|
+
type: "refresh_verification",
|
|
231
|
+
priority: input.verification.requiredCheckCount === 0 ? "high" : "medium",
|
|
232
|
+
message: "Refresh the verification plan so the next attempt has explicit required checks before any completion claim."
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
if (input.anomalyCount > 0) {
|
|
236
|
+
actions.push({
|
|
237
|
+
type: "investigate_anomalies",
|
|
238
|
+
priority: input.anomalyCount >= 2 ? "high" : "medium",
|
|
239
|
+
message: "Inspect the latest anomaly evidence before retrying so the loop does not reuse a drifting or contradictory context frame."
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (input.touchedSurfaceCount >= 9) {
|
|
243
|
+
actions.push({
|
|
244
|
+
type: "narrow_surfaces",
|
|
245
|
+
priority: "medium",
|
|
246
|
+
message: "Narrow the next patch to the minimum changed surfaces because the current context spans too many files."
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return actions;
|
|
250
|
+
}
|
|
251
|
+
function buildSummary(input) {
|
|
252
|
+
if (input.utilityBand === "low") {
|
|
253
|
+
return `Low context utility (${String(input.utilityScore)}/100). Compress the active context, refresh verification focus, and evict ${String(input.evictableMemoryCount)} stale memory drawer(s) before retrying.`;
|
|
254
|
+
}
|
|
255
|
+
if (input.utilityBand === "medium") {
|
|
256
|
+
return `Medium context utility (${String(input.utilityScore)}/100). Keep the contract and verification cues, prune ${String(input.evictableMemoryCount)} stale memory drawer(s), and review ${String(input.anomalyCount)} anomaly guard(s).`;
|
|
257
|
+
}
|
|
258
|
+
const anomalyNote = input.anomalyCount > 0
|
|
259
|
+
? `review ${String(input.anomalyCount)} anomaly guard(s)`
|
|
260
|
+
: "keep the current anomaly guard state";
|
|
261
|
+
return `High context utility (${String(input.utilityScore)}/100). Contract and verification signals are strong; retain the best memory cues, evict ${String(input.evictableMemoryCount)} stale drawer(s), and ${anomalyNote}.`;
|
|
262
|
+
}
|
|
263
|
+
function resolveVerificationCounts(compiledContext, verificationPlan) {
|
|
264
|
+
const requiredFromArtifact = collectCheckCount(verificationPlan, [
|
|
265
|
+
"requiredVerifiers",
|
|
266
|
+
"requiredChecks",
|
|
267
|
+
"required",
|
|
268
|
+
"mustRun",
|
|
269
|
+
"verificationPlan"
|
|
270
|
+
]);
|
|
271
|
+
const optionalFromArtifact = collectCheckCount(verificationPlan, [
|
|
272
|
+
"optionalChecks",
|
|
273
|
+
"optionalVerifiers",
|
|
274
|
+
"optional",
|
|
275
|
+
"recommendedVerifiers",
|
|
276
|
+
"fallbackVerifiers"
|
|
277
|
+
]);
|
|
278
|
+
const requiredCheckCount = requiredFromArtifact > 0
|
|
279
|
+
? requiredFromArtifact
|
|
280
|
+
: compiledContext.contract.verificationPlan.length;
|
|
281
|
+
const optionalCheckCount = optionalFromArtifact;
|
|
282
|
+
return {
|
|
283
|
+
requiredCheckCount,
|
|
284
|
+
optionalCheckCount,
|
|
285
|
+
hasTieredPlan: isRecord(verificationPlan) &&
|
|
286
|
+
typeof verificationPlan["tier"] === "string",
|
|
287
|
+
mustPassAll: isRecord(verificationPlan) &&
|
|
288
|
+
verificationPlan["mustPassAll"] === true
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function resolvePatterns(patterns) {
|
|
292
|
+
if (Array.isArray(patterns)) {
|
|
293
|
+
return patterns.filter(isPatternDetection);
|
|
294
|
+
}
|
|
295
|
+
if (isRecord(patterns) && Array.isArray(patterns["patterns"])) {
|
|
296
|
+
return patterns["patterns"].filter(isPatternDetection);
|
|
297
|
+
}
|
|
298
|
+
return [];
|
|
299
|
+
}
|
|
300
|
+
function resolveScopeSurface(scopeSurface) {
|
|
301
|
+
if (!isRecord(scopeSurface)) {
|
|
302
|
+
return {
|
|
303
|
+
touchedSurfaces: [],
|
|
304
|
+
changedFileCount: 0
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const touchedSurfaces = uniqueNormalizedPaths([
|
|
308
|
+
...readStringArray(scopeSurface["allChangedPaths"]),
|
|
309
|
+
...readStringArray(scopeSurface["trackedChangedPaths"]),
|
|
310
|
+
...readStringArray(scopeSurface["deletedPaths"]),
|
|
311
|
+
...readStringArray(scopeSurface["newUntrackedPaths"])
|
|
312
|
+
]);
|
|
313
|
+
const diffStats = isRecord(scopeSurface["diffStats"]) ? scopeSurface["diffStats"] : undefined;
|
|
314
|
+
const changedFileCount = typeof diffStats?.["filesChanged"] === "number"
|
|
315
|
+
? diffStats["filesChanged"]
|
|
316
|
+
: touchedSurfaces.length;
|
|
317
|
+
return {
|
|
318
|
+
touchedSurfaces,
|
|
319
|
+
changedFileCount
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function scoreMemoryEntries(compiledContext, entries) {
|
|
323
|
+
const promptTokens = buildPromptTokens(compiledContext);
|
|
324
|
+
return entries.map((entry) => {
|
|
325
|
+
let utility = Math.min(Math.max(entry.score, 0), 60);
|
|
326
|
+
const reason = entry.reason.toLowerCase();
|
|
327
|
+
if (reason.includes("matched failure class")) {
|
|
328
|
+
utility += 20;
|
|
329
|
+
}
|
|
330
|
+
if (reason.includes("matched intervention")) {
|
|
331
|
+
utility += 10;
|
|
332
|
+
}
|
|
333
|
+
const overlap = countIntersection(promptTokens, buildEntryTokens(entry));
|
|
334
|
+
utility += overlap * 6;
|
|
335
|
+
return {
|
|
336
|
+
entry,
|
|
337
|
+
utility: clampScore(utility)
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
function buildPromptTokens(compiledContext) {
|
|
342
|
+
return uniqueTokens([
|
|
343
|
+
...tokenize(compiledContext.contract.objective),
|
|
344
|
+
...compiledContext.contract.verificationPlan.flatMap((command) => tokenize(command)),
|
|
345
|
+
...tokenize(compiledContext.guidance),
|
|
346
|
+
...compiledContext.priorFailurePatterns.flatMap((pattern) => tokenize(pattern))
|
|
347
|
+
]);
|
|
348
|
+
}
|
|
349
|
+
function buildEntryTokens(entry) {
|
|
350
|
+
return uniqueTokens([
|
|
351
|
+
...tokenize(entry.roomKey),
|
|
352
|
+
...tokenize(entry.lesson),
|
|
353
|
+
...tokenize(entry.content),
|
|
354
|
+
...tokenize(entry.reason)
|
|
355
|
+
]);
|
|
356
|
+
}
|
|
357
|
+
function collectCheckCount(value, keys) {
|
|
358
|
+
if (Array.isArray(value)) {
|
|
359
|
+
return value.length;
|
|
360
|
+
}
|
|
361
|
+
if (!isRecord(value)) {
|
|
362
|
+
return 0;
|
|
363
|
+
}
|
|
364
|
+
return keys.reduce((sum, key) => {
|
|
365
|
+
const next = value[key];
|
|
366
|
+
return sum + (Array.isArray(next) ? next.length : 0);
|
|
367
|
+
}, 0);
|
|
368
|
+
}
|
|
369
|
+
function isPatternDetection(value) {
|
|
370
|
+
return (isRecord(value) &&
|
|
371
|
+
typeof value["attemptId"] === "string" &&
|
|
372
|
+
typeof value["patternId"] === "string" &&
|
|
373
|
+
typeof value["message"] === "string");
|
|
374
|
+
}
|
|
375
|
+
function readStringArray(value) {
|
|
376
|
+
return Array.isArray(value)
|
|
377
|
+
? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
|
|
378
|
+
: [];
|
|
379
|
+
}
|
|
380
|
+
function readString(value) {
|
|
381
|
+
return typeof value === "string" ? value : undefined;
|
|
382
|
+
}
|
|
383
|
+
function readFiniteNumber(value) {
|
|
384
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
385
|
+
}
|
|
386
|
+
function tokenize(value) {
|
|
387
|
+
return (value.toLowerCase().match(/[a-z0-9_/-]{3,}/g) ?? []).filter((token) => token.trim().length > 0);
|
|
388
|
+
}
|
|
389
|
+
function uniqueTokens(values) {
|
|
390
|
+
return [...new Set(values)];
|
|
391
|
+
}
|
|
392
|
+
function uniqueNormalizedPaths(paths) {
|
|
393
|
+
return [...new Set(paths.map((path) => path.replace(/\\/g, "/").replace(/^\.\//u, "")))].sort();
|
|
394
|
+
}
|
|
395
|
+
function countIntersection(left, right) {
|
|
396
|
+
const rightSet = new Set(right);
|
|
397
|
+
return left.filter((token) => rightSet.has(token)).length;
|
|
398
|
+
}
|
|
399
|
+
function clampScore(value) {
|
|
400
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
401
|
+
}
|
|
402
|
+
function isRecord(value) {
|
|
403
|
+
return typeof value === "object" && value !== null;
|
|
404
|
+
}
|
|
405
|
+
//# sourceMappingURL=context-utility.js.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable cost-aware LLM pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Combines four patterns into one composable function:
|
|
5
|
+
* 1. Model routing by task complexity (Haiku vs Sonnet)
|
|
6
|
+
* 2. Immutable cost tracking with budget enforcement
|
|
7
|
+
* 3. Narrow retry logic (transient errors only, exponential backoff)
|
|
8
|
+
* 4. Prompt caching via Anthropic cache_control messages
|
|
9
|
+
*
|
|
10
|
+
* Does NOT import @anthropic-ai/sdk — uses an injected LlmClient interface
|
|
11
|
+
* so it works with the claude-cli adapter or any HTTP client.
|
|
12
|
+
*/
|
|
13
|
+
export declare const MODEL_HAIKU: "claude-haiku-4-5-20251001";
|
|
14
|
+
export declare const MODEL_SONNET: "claude-sonnet-4-6";
|
|
15
|
+
export type SupportedModel = typeof MODEL_HAIKU | typeof MODEL_SONNET;
|
|
16
|
+
/**
|
|
17
|
+
* Select the cheapest model that can handle the task complexity.
|
|
18
|
+
* Returns forceModel unchanged when provided.
|
|
19
|
+
*/
|
|
20
|
+
export declare function selectModel(textLength: number, itemCount: number, forceModel?: string): string;
|
|
21
|
+
export interface CostRecord {
|
|
22
|
+
readonly model: string;
|
|
23
|
+
readonly inputTokens: number;
|
|
24
|
+
readonly outputTokens: number;
|
|
25
|
+
readonly costUsd: number;
|
|
26
|
+
readonly timestamp: string;
|
|
27
|
+
}
|
|
28
|
+
export interface CostTracker {
|
|
29
|
+
readonly budgetLimitUsd: number;
|
|
30
|
+
readonly records: readonly CostRecord[];
|
|
31
|
+
readonly totalCost: number;
|
|
32
|
+
readonly overBudget: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare function createCostTracker(budgetLimitUsd: number): CostTracker;
|
|
35
|
+
export declare function addCostRecord(tracker: CostTracker, record: Omit<CostRecord, "timestamp">): CostTracker;
|
|
36
|
+
export interface RetryOptions {
|
|
37
|
+
readonly maxRetries?: number;
|
|
38
|
+
readonly baseDelayMs?: number;
|
|
39
|
+
}
|
|
40
|
+
export declare function callWithRetry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
41
|
+
export interface CacheControlEphemeral {
|
|
42
|
+
readonly type: "ephemeral";
|
|
43
|
+
}
|
|
44
|
+
export interface TextBlockWithCache {
|
|
45
|
+
readonly type: "text";
|
|
46
|
+
readonly text: string;
|
|
47
|
+
readonly cache_control: CacheControlEphemeral;
|
|
48
|
+
}
|
|
49
|
+
export interface SystemMessage {
|
|
50
|
+
readonly role: "system";
|
|
51
|
+
readonly content: readonly [TextBlockWithCache];
|
|
52
|
+
}
|
|
53
|
+
export interface UserMessage {
|
|
54
|
+
readonly role: "user";
|
|
55
|
+
readonly content: string;
|
|
56
|
+
}
|
|
57
|
+
export type CachedMessageArray = readonly [SystemMessage, UserMessage];
|
|
58
|
+
export declare function buildCachedMessages(systemPrompt: string, userInput: string): CachedMessageArray;
|
|
59
|
+
export interface LlmCallResult {
|
|
60
|
+
readonly content: string;
|
|
61
|
+
readonly inputTokens: number;
|
|
62
|
+
readonly outputTokens: number;
|
|
63
|
+
readonly costUsd: number;
|
|
64
|
+
}
|
|
65
|
+
export interface LlmClient {
|
|
66
|
+
call(model: string, messages: CachedMessageArray, options?: {
|
|
67
|
+
maxTokens?: number;
|
|
68
|
+
}): Promise<LlmCallResult>;
|
|
69
|
+
}
|
|
70
|
+
export interface PipelineConfig {
|
|
71
|
+
readonly forceModel?: string;
|
|
72
|
+
readonly maxRetries?: number;
|
|
73
|
+
readonly baseDelayMs?: number;
|
|
74
|
+
readonly maxTokens?: number;
|
|
75
|
+
}
|
|
76
|
+
export interface PipelineInput {
|
|
77
|
+
readonly text: string;
|
|
78
|
+
readonly config?: PipelineConfig;
|
|
79
|
+
readonly tracker: CostTracker;
|
|
80
|
+
readonly client: LlmClient;
|
|
81
|
+
readonly systemPrompt: string;
|
|
82
|
+
}
|
|
83
|
+
export interface PipelineOutput {
|
|
84
|
+
readonly result: LlmCallResult;
|
|
85
|
+
readonly tracker: CostTracker;
|
|
86
|
+
}
|
|
87
|
+
export declare class BudgetExceededError extends Error {
|
|
88
|
+
readonly totalCost: number;
|
|
89
|
+
readonly budgetLimit: number;
|
|
90
|
+
constructor(totalCost: number, budgetLimit: number);
|
|
91
|
+
}
|
|
92
|
+
export declare function runCostAwarePipeline(input: PipelineInput): Promise<PipelineOutput>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable cost-aware LLM pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Combines four patterns into one composable function:
|
|
5
|
+
* 1. Model routing by task complexity (Haiku vs Sonnet)
|
|
6
|
+
* 2. Immutable cost tracking with budget enforcement
|
|
7
|
+
* 3. Narrow retry logic (transient errors only, exponential backoff)
|
|
8
|
+
* 4. Prompt caching via Anthropic cache_control messages
|
|
9
|
+
*
|
|
10
|
+
* Does NOT import @anthropic-ai/sdk — uses an injected LlmClient interface
|
|
11
|
+
* so it works with the claude-cli adapter or any HTTP client.
|
|
12
|
+
*/
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Model constants
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
export const MODEL_HAIKU = "claude-haiku-4-5-20251001";
|
|
17
|
+
export const MODEL_SONNET = "claude-sonnet-4-6";
|
|
18
|
+
const COMPLEXITY_CHAR_THRESHOLD = 10_000;
|
|
19
|
+
const COMPLEXITY_ITEM_THRESHOLD = 30;
|
|
20
|
+
// Private pricing for budget preflight estimation only.
|
|
21
|
+
// Canonical pricing lives in packages/adapters/src/usage.ts.
|
|
22
|
+
const PIPELINE_PRICING = {
|
|
23
|
+
[MODEL_HAIKU]: { inputPer1K: 0.00025, outputPer1K: 0.00125 },
|
|
24
|
+
[MODEL_SONNET]: { inputPer1K: 0.003, outputPer1K: 0.015 }
|
|
25
|
+
};
|
|
26
|
+
const FALLBACK_PRICING = { inputPer1K: 0.003, outputPer1K: 0.015 };
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// 1. Model routing
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/**
|
|
31
|
+
* Select the cheapest model that can handle the task complexity.
|
|
32
|
+
* Returns forceModel unchanged when provided.
|
|
33
|
+
*/
|
|
34
|
+
export function selectModel(textLength, itemCount, forceModel) {
|
|
35
|
+
if (forceModel !== undefined)
|
|
36
|
+
return forceModel;
|
|
37
|
+
if (textLength >= COMPLEXITY_CHAR_THRESHOLD || itemCount >= COMPLEXITY_ITEM_THRESHOLD) {
|
|
38
|
+
return MODEL_SONNET;
|
|
39
|
+
}
|
|
40
|
+
return MODEL_HAIKU;
|
|
41
|
+
}
|
|
42
|
+
export function createCostTracker(budgetLimitUsd) {
|
|
43
|
+
return makeFrozenTracker(budgetLimitUsd, []);
|
|
44
|
+
}
|
|
45
|
+
export function addCostRecord(tracker, record) {
|
|
46
|
+
const full = Object.freeze({
|
|
47
|
+
...record,
|
|
48
|
+
timestamp: new Date().toISOString()
|
|
49
|
+
});
|
|
50
|
+
return makeFrozenTracker(tracker.budgetLimitUsd, [...tracker.records, full]);
|
|
51
|
+
}
|
|
52
|
+
function makeFrozenTracker(budgetLimitUsd, records) {
|
|
53
|
+
const totalCost = records.reduce((sum, r) => sum + r.costUsd, 0);
|
|
54
|
+
return Object.freeze({
|
|
55
|
+
budgetLimitUsd,
|
|
56
|
+
records: Object.freeze(records),
|
|
57
|
+
totalCost,
|
|
58
|
+
overBudget: totalCost > budgetLimitUsd
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function classifyError(err) {
|
|
62
|
+
const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
|
|
63
|
+
if (/econnreset|etimedout|enotfound|econnrefused|socket hang up/.test(msg))
|
|
64
|
+
return "retryable";
|
|
65
|
+
if (/429|rate.limit|too.many.requests/.test(msg))
|
|
66
|
+
return "retryable";
|
|
67
|
+
if (/5\d{2}|internal.server.error|overloaded/.test(msg))
|
|
68
|
+
return "retryable";
|
|
69
|
+
return "fatal";
|
|
70
|
+
}
|
|
71
|
+
export async function callWithRetry(fn, options = {}) {
|
|
72
|
+
const maxRetries = options.maxRetries ?? 3;
|
|
73
|
+
const baseDelayMs = options.baseDelayMs ?? 1_000;
|
|
74
|
+
let lastErr;
|
|
75
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
76
|
+
try {
|
|
77
|
+
return await fn();
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
if (classifyError(err) === "fatal")
|
|
81
|
+
throw err;
|
|
82
|
+
lastErr = err;
|
|
83
|
+
if (attempt < maxRetries - 1) {
|
|
84
|
+
const delay = Math.min(baseDelayMs * Math.pow(2, attempt), 32_000);
|
|
85
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
throw lastErr;
|
|
90
|
+
}
|
|
91
|
+
export function buildCachedMessages(systemPrompt, userInput) {
|
|
92
|
+
const systemMsg = Object.freeze({
|
|
93
|
+
role: "system",
|
|
94
|
+
content: Object.freeze([
|
|
95
|
+
Object.freeze({
|
|
96
|
+
type: "text",
|
|
97
|
+
text: systemPrompt,
|
|
98
|
+
cache_control: Object.freeze({ type: "ephemeral" })
|
|
99
|
+
})
|
|
100
|
+
])
|
|
101
|
+
});
|
|
102
|
+
const userMsg = Object.freeze({
|
|
103
|
+
role: "user",
|
|
104
|
+
content: userInput
|
|
105
|
+
});
|
|
106
|
+
return Object.freeze([systemMsg, userMsg]);
|
|
107
|
+
}
|
|
108
|
+
export class BudgetExceededError extends Error {
|
|
109
|
+
totalCost;
|
|
110
|
+
budgetLimit;
|
|
111
|
+
constructor(totalCost, budgetLimit) {
|
|
112
|
+
super(`Budget exceeded: spent $${totalCost.toFixed(4)} of $${budgetLimit.toFixed(4)}`);
|
|
113
|
+
this.totalCost = totalCost;
|
|
114
|
+
this.budgetLimit = budgetLimit;
|
|
115
|
+
this.name = "BudgetExceededError";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export async function runCostAwarePipeline(input) {
|
|
119
|
+
const { text, config, tracker, client, systemPrompt } = input;
|
|
120
|
+
const itemCount = text.split("\n").length;
|
|
121
|
+
const model = selectModel(text.length, itemCount, config?.forceModel);
|
|
122
|
+
// Preflight budget check: estimate cost before spending
|
|
123
|
+
const estimatedInputTokens = Math.ceil(text.length / 3.5);
|
|
124
|
+
const estimatedOutputTokens = 500;
|
|
125
|
+
const pricing = PIPELINE_PRICING[model] ?? FALLBACK_PRICING;
|
|
126
|
+
const estimatedCost = (estimatedInputTokens / 1000) * pricing.inputPer1K +
|
|
127
|
+
(estimatedOutputTokens / 1000) * pricing.outputPer1K;
|
|
128
|
+
if (tracker.totalCost + estimatedCost > tracker.budgetLimitUsd) {
|
|
129
|
+
throw new BudgetExceededError(tracker.totalCost, tracker.budgetLimitUsd);
|
|
130
|
+
}
|
|
131
|
+
const messages = buildCachedMessages(systemPrompt, text);
|
|
132
|
+
const result = await callWithRetry(() => client.call(model, messages, { maxTokens: config?.maxTokens }), { maxRetries: config?.maxRetries, baseDelayMs: config?.baseDelayMs });
|
|
133
|
+
const updatedTracker = addCostRecord(tracker, {
|
|
134
|
+
model,
|
|
135
|
+
inputTokens: result.inputTokens,
|
|
136
|
+
outputTokens: result.outputTokens,
|
|
137
|
+
costUsd: result.costUsd
|
|
138
|
+
});
|
|
139
|
+
return { result, tracker: updatedTracker };
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=pipeline.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface TaggedCostEntry {
|
|
2
|
+
runId: string;
|
|
3
|
+
tags: Record<string, string>;
|
|
4
|
+
totalUsd: number;
|
|
5
|
+
inputTokens: number;
|
|
6
|
+
outputTokens: number;
|
|
7
|
+
timestamp: string;
|
|
8
|
+
}
|
|
9
|
+
export interface TaggedCostSummary {
|
|
10
|
+
tag: string;
|
|
11
|
+
value: string;
|
|
12
|
+
totalUsd: number;
|
|
13
|
+
runCount: number;
|
|
14
|
+
entries: TaggedCostEntry[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Groups cost entries by the value of a specific tag key.
|
|
18
|
+
* Entries missing the tag key are grouped under "(untagged)".
|
|
19
|
+
* Results are sorted descending by totalUsd.
|
|
20
|
+
*/
|
|
21
|
+
export declare function aggregateCostsByTag(entries: TaggedCostEntry[], tagKey: string): TaggedCostSummary[];
|
|
22
|
+
/**
|
|
23
|
+
* Formats a list of TaggedCostSummary objects as a plain-text table.
|
|
24
|
+
* Columns: tag value | runs | total USD
|
|
25
|
+
* Sorted descending by totalUsd (aggregateCostsByTag already sorts).
|
|
26
|
+
*/
|
|
27
|
+
export declare function formatCostReport(summaries: TaggedCostSummary[]): string;
|